├── .env.example ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.md ├── README-zh.md ├── README.md ├── assets └── img │ └── preview.png ├── build.bat ├── build.mac.command ├── build.py ├── build.sh ├── config.py ├── data ├── emails.txt └── names-dataset.txt ├── emails.txt ├── generate_email.py ├── logger.py ├── requirements.txt ├── src ├── __init__.py ├── auth │ ├── __init__.py │ ├── cursor_auth_manager.py │ ├── patch_cursor_get_machine_id.py │ └── reset_machine.py ├── core │ ├── __init__.py │ ├── cursor_pro_keep_alive.py │ ├── exit_cursor.py │ └── go_cursor_help.py ├── icloud │ ├── __init__.py │ ├── deleteEmail.py │ ├── generateEmail.py │ └── hidemyemail.py ├── main.py ├── turnstilePatch │ ├── manifest.json │ ├── readme.txt │ └── script.js ├── ui │ ├── __init__.py │ └── logo.py └── utils │ ├── __init__.py │ ├── browser_utils.py │ ├── config.py │ ├── get_email_code.py │ ├── language.py │ └── logger.py └── test ├── test_delete_email.py └── test_email.py /.env.example: -------------------------------------------------------------------------------- 1 | BROWSER_USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.92 Safari/537.36 2 | 3 | # zh or en 4 | LANGUAGE=zh 5 | 6 | # 无头模式 默认开启 7 | # BROWSER_HEADLESS='True' 8 | 9 | # 使用其他浏览器(如Edge) 10 | # BROWSER_PATH='C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe' 11 | 12 | 13 | ICLOUD_USER= 14 | ICLOUD_APP_PASSWORD= 15 | ICLOUD_COOKIES= 16 | 17 | 18 | # Custom paths for data files (optional) 19 | # EMAIL_FILE_PATH=C:/path/to/your/emails.txt 20 | # CSV_FILE_PATH=C:/path/to/your/accounts.csv 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Executables 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # Trigger on tags like v1.0.0 7 | workflow_dispatch: # Allow manual triggering of the workflow 8 | 9 | jobs: 10 | build-windows: 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: "3.9" 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install pyinstaller 25 | pip install -r requirements.txt 26 | 27 | - name: Build Windows executable 28 | run: | 29 | # Set PowerShell to use UTF-8 encoding 30 | [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 31 | $env:PYTHONIOENCODING = "utf-8" 32 | python build.py --platform windows 33 | 34 | # Check if the output directory exists and has files 35 | if (Test-Path "dist/windows") { 36 | Write-Host "Windows output directory exists" 37 | Get-ChildItem "dist/windows" -Recurse | ForEach-Object { 38 | Write-Host " - $($_.FullName) (Size: $($_.Length) bytes)" 39 | } 40 | 41 | # Check if directory is empty 42 | if (!(Get-ChildItem "dist/windows")) { 43 | Write-Host "Warning: Output directory is empty, creating placeholder file" 44 | "Placeholder file to prevent artifact upload failure" | Out-File -FilePath "dist/windows/placeholder.txt" 45 | } 46 | } else { 47 | Write-Host "Warning: Output directory not found, creating it with a placeholder file" 48 | New-Item -ItemType Directory -Path "dist/windows" -Force 49 | "Placeholder file to prevent artifact upload failure" | Out-File -FilePath "dist/windows/placeholder.txt" 50 | } 51 | 52 | - name: Upload Windows artifact 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: CursorKeepAlive-Windows 56 | path: dist/windows/ 57 | if-no-files-found: warn 58 | 59 | build-macos-arm64: 60 | runs-on: macos-latest 61 | 62 | steps: 63 | - uses: actions/checkout@v2 64 | 65 | - name: Set up Python 66 | uses: actions/setup-python@v2 67 | with: 68 | python-version: "3.9" 69 | 70 | - name: Install dependencies 71 | run: | 72 | python -m pip install --upgrade pip 73 | pip install pyinstaller 74 | pip install -r requirements.txt 75 | 76 | - name: Build MacOS ARM executable 77 | run: | 78 | python build.py --platform mac_m1 --arch arm64 79 | 80 | # Check if the output directory exists and has files 81 | if [ -d "dist/mac_m1" ]; then 82 | echo "MacOS ARM output directory exists" 83 | find "dist/mac_m1" -type f -exec ls -la {} \; 84 | 85 | # Check if directory is empty 86 | if [ ! "$(ls -A dist/mac_m1)" ]; then 87 | echo "Warning: Output directory is empty, creating placeholder file" 88 | echo "Placeholder file to prevent artifact upload failure" > "dist/mac_m1/placeholder.txt" 89 | fi 90 | else 91 | echo "Warning: Output directory not found, creating it with a placeholder file" 92 | mkdir -p "dist/mac_m1" 93 | echo "Placeholder file to prevent artifact upload failure" > "dist/mac_m1/placeholder.txt" 94 | fi 95 | 96 | - name: Upload MacOS ARM artifact 97 | uses: actions/upload-artifact@v4 98 | with: 99 | name: CursorKeepAlive-MacOS-ARM64 100 | path: dist/mac_m1/ 101 | if-no-files-found: warn 102 | 103 | build-linux: 104 | runs-on: ubuntu-latest 105 | 106 | steps: 107 | - uses: actions/checkout@v2 108 | 109 | - name: Set up Python 110 | uses: actions/setup-python@v2 111 | with: 112 | python-version: "3.9" 113 | 114 | - name: Install dependencies 115 | run: | 116 | python -m pip install --upgrade pip 117 | pip install pyinstaller 118 | pip install -r requirements.txt 119 | 120 | - name: Build Linux executable 121 | run: | 122 | python build.py --platform linux 123 | 124 | # Check if the output directory exists and has files 125 | if [ -d "dist/linux" ]; then 126 | echo "Linux output directory exists" 127 | find "dist/linux" -type f -exec ls -la {} \; 128 | 129 | # Check if directory is empty 130 | if [ ! "$(ls -A dist/linux)" ]; then 131 | echo "Warning: Output directory is empty, creating placeholder file" 132 | echo "Placeholder file to prevent artifact upload failure" > "dist/linux/placeholder.txt" 133 | fi 134 | else 135 | echo "Warning: Output directory not found, creating it with a placeholder file" 136 | mkdir -p "dist/linux" 137 | echo "Placeholder file to prevent artifact upload failure" > "dist/linux/placeholder.txt" 138 | fi 139 | 140 | - name: Upload Linux artifact 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: CursorKeepAlive-Linux 144 | path: dist/linux/ 145 | if-no-files-found: warn 146 | 147 | build-macos-intel: 148 | runs-on: macos-latest 149 | 150 | steps: 151 | - uses: actions/checkout@v2 152 | 153 | - name: Set up Python 154 | uses: actions/setup-python@v2 155 | with: 156 | python-version: "3.9" 157 | 158 | - name: Install dependencies 159 | run: | 160 | python -m pip install --upgrade pip 161 | pip install pyinstaller 162 | pip install -r requirements.txt 163 | 164 | - name: Build MacOS Intel executable 165 | run: | 166 | python build.py --platform mac_intel 167 | 168 | # Check if the output directory exists and has files 169 | if [ -d "dist/mac_intel" ]; then 170 | echo "MacOS Intel output directory exists" 171 | find "dist/mac_intel" -type f -exec ls -la {} \; 172 | 173 | # Check if directory is empty 174 | if [ ! "$(ls -A dist/mac_intel)" ]; then 175 | echo "Warning: Output directory is empty, creating placeholder file" 176 | echo "Placeholder file to prevent artifact upload failure" > "dist/mac_intel/placeholder.txt" 177 | fi 178 | else 179 | echo "Warning: Output directory not found, creating it with a placeholder file" 180 | mkdir -p "dist/mac_intel" 181 | echo "Placeholder file to prevent artifact upload failure" > "dist/mac_intel/placeholder.txt" 182 | fi 183 | 184 | - name: Upload MacOS Intel artifact 185 | uses: actions/upload-artifact@v4 186 | with: 187 | name: CursorKeepAlive-MacOS-Intel 188 | path: dist/mac_intel/ 189 | if-no-files-found: warn 190 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PyInstaller 2 | build/ 3 | dist/ 4 | *.spec 5 | !CursorKeepAlive.mac.spec 6 | !CursorKeepAlive.win.spec 7 | 8 | # Python 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | **/__pycache__/ 13 | 14 | 15 | # Logs 16 | *.log 17 | 18 | # IDE 19 | .vscode/ 20 | .idea/ 21 | 22 | # Mac 23 | .DS_Store 24 | 25 | venv/ 26 | 27 | node_modules/ 28 | 29 | .env 30 | 31 | screenshots/ 32 | 33 | accounts.csv -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Attribution-NonCommercial-NoDerivatives 4.0 International 2 | 3 | > *Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.* 4 | > 5 | > ### Using Creative Commons Public Licenses 6 | > 7 | > Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 8 | > 9 | > * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 10 | > 11 | > * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 12 | 13 | ## Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License 14 | 15 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 16 | 17 | ### Section 1 – Definitions. 18 | 19 | a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 20 | 21 | b. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 22 | 23 | e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 24 | 25 | f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 26 | 27 | h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 28 | 29 | i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 30 | 31 | h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. 32 | 33 | i. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 34 | 35 | j. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 36 | 37 | k. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 38 | 39 | l. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 40 | 41 | ### Section 2 – Scope. 42 | 43 | a. ___License grant.___ 44 | 45 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 46 | 47 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 48 | 49 | B. produce and reproduce, but not Share, Adapted Material for NonCommercial purposes only. 50 | 51 | 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 52 | 53 | 3. __Term.__ The term of this Public License is specified in Section 6(a). 54 | 55 | 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 56 | 57 | 5. __Downstream recipients.__ 58 | 59 | A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 60 | 61 | B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 62 | 63 | 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 64 | 65 | b. ___Other rights.___ 66 | 67 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 68 | 69 | 2. Patent and trademark rights are not licensed under this Public License. 70 | 71 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 72 | 73 | ### Section 3 – License Conditions. 74 | 75 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 76 | 77 | a. ___Attribution.___ 78 | 79 | 1. If You Share the Licensed Material, You must: 80 | 81 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 82 | 83 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 84 | 85 | ii. a copyright notice; 86 | 87 | iii. a notice that refers to this Public License; 88 | 89 | iv. a notice that refers to the disclaimer of warranties; 90 | 91 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 92 | 93 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 94 | 95 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 96 | 97 | For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material. 98 | 99 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 100 | 101 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 102 | 103 | ### Section 4 – Sui Generis Database Rights. 104 | 105 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 106 | 107 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only and provided You do not Share Adapted Material; 108 | 109 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 110 | 111 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 112 | 113 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 114 | 115 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 116 | 117 | a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ 118 | 119 | b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ 120 | 121 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 122 | 123 | ### Section 6 – Term and Termination. 124 | 125 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 126 | 127 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 128 | 129 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 130 | 131 | 2. upon express reinstatement by the Licensor. 132 | 133 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 134 | 135 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 136 | 137 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 138 | 139 | ### Section 7 – Other Terms and Conditions. 140 | 141 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 142 | 143 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 144 | 145 | ### Section 8 – Interpretation. 146 | 147 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 148 | 149 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 150 | 151 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 152 | 153 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 154 | 155 | > Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 156 | > 157 | > Creative Commons may be contacted at [creativecommons.org](http://creativecommons.org). 158 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # Cursor Pro (iCloud) 自动化工具 - 免费试用重置工具 2 | 3 |
4 | 5 | [![Release](https://img.shields.io/github/v/release/ryan0204/cursor-auto-icloud?style=flat-square&logo=github&color=blue)](https://github.com/ryan0204/cursor-auto-icloud/releases/latest) 6 | [![Stars](https://img.shields.io/github/stars/ryan0204/cursor-auto-icloud?style=flat-square&logo=github)](https://github.com/ryan0204/cursor-auto-icloud/stargazers) 7 | 8 | 9 | ## ⭐️ 在 GitHub 上给我们 Star — 这对我们是很大的鼓励! 10 | 11 | [🌏 English README](README.md) 12 | 13 | Cursor Logo 14 | 15 | Tool Preview 16 | 17 |
18 | 19 | ## 目录 20 | 21 | - [准备工作](#准备工作) 22 | - [下载](#下载) 23 | - [设置](#设置) 24 | - [运行工具](#运行工具) 25 | - [免责声明](#免责声明) 26 | - [致谢](#致谢) 27 | - [贡献](#贡献) 28 | - [许可证](#许可证) 29 | 30 | ## 准备工作 31 | 32 | 在使用此工具之前,您应该准备以下内容: 33 | 34 | - 一个拥有 **iCloud Plus** 的苹果账号 (邮箱后缀为 @icloud.com) 35 | 36 | ## 下载 37 | 38 | 1. 从 GitHub Releases 下载最新版本 39 | 2. 根据你的系统选择对应的版本: 40 | 41 | > Windows:直接下载 CursorKeepAlive.exe 42 | > Mac(Intel):选择 x64 版本 43 | > Mac(M系列):选择 ARM64(aarch64) 版本 44 | 45 | ### Mac 用户额外步骤 46 | 47 | > 打开终端,进入应用所在目录 48 | > 执行以下命令使文件可执行: 49 | > ```chmod +x ./CursorKeepAlive``` 50 | 51 | 按照下文设置,然后运行 52 | 53 | ## 设置 54 | 55 | ### 设置环境变量 56 | 57 | > Mac 用户:如果您无法重命名文件,可以使用 `touch .env` 在同一目录中创建该文件。 58 | 59 | 1. 下载 [`.env.example`](https://github.com/Ryan0204/cursor-auto-icloud/blob/main/.env.example) 文件并将其重命名为 `.env` 60 | 2. 填写 `.env` 文件 61 | 62 | ```env 63 | ICLOUD_USER=您的苹果ID(!!! 不包括 @icloud.com) 64 | ICLOUD_APP_PASSWORD=您的苹果ID应用专用密码(解释如下) 65 | ICLOUD_COOKIES=您的iCloud cookies(解释如下) 66 | ``` 67 | 68 | ### 获取 iCloud cookie 字符串 69 | 70 | 1. 下载 [Cookie-Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) Chrome 扩展 71 | 2. 在浏览器中到 [iCloud 设置](https://www.icloud.com/settings/) 并登录 72 | 3. 点击 Cookie-Editor 扩展并以 `Header String` 格式导出 cookies 73 | 4. 将导出的 cookies 粘贴到名为 `.env` 的文件中作为 `ICLOUD_COOKIES` 74 | 75 | Cookie 示例: 76 | 77 | ``` 78 | X_APPLE_WEB_KB-V5590FJFX4ZYGBSJEZRZBTFB9UU="xxxxxx";X-APPLE-DS-WEB-SESSION-TOKEN="xxxxxx";X-APPLE-UNIQUE-CLIENT-ID="xxxxxx";X-APPLE-WEB-ID=28672BD9012631BA3CBAE022A1DBAEE2D0AFD358;X-APPLE-WEBAUTH-HSA-TRUST="xxxxxx";X-APPLE-WEBAUTH-LOGIN="xxxxxx";X-APPLE-WEBAUTH-PCS-Cloudkit="xxxxxx";X-APPLE-WEBAUTH-PCS-Documents="xxxxxx";X-APPLE-WEBAUTH-PCS-Mail="xxxxxx";X-APPLE-WEBAUTH-PCS-News="xxxxxx";X-APPLE-WEBAUTH-PCS-Notes="xxxxxx";X-APPLE-WEBAUTH-PCS-Photos="xxxxxx";X-APPLE-WEBAUTH-PCS-Safari="xxxxxx";X-APPLE-WEBAUTH-PCS-Sharing="xxxxxx";X-APPLE-WEBAUTH-TOKEN="xxxxxx";X-APPLE-WEBAUTH-USER="xxxxxx";X-APPLE-WEBAUTH-VALIDATE="xxxxxx"; 79 | ``` 80 | 81 | ### 获取 Apple ID 应用专用密码 82 | 83 | 1. 在 [account.apple.com](https://account.apple.com) 登录您的 Apple 账户 84 | 2. 在登录和安全部分,选择应用专用密码 85 | 3. 选择生成应用专用密码,然后按照屏幕上的步骤操作 86 | 4. 复制生成的密码并将其粘贴到名为 `.env` 的文件中作为 `ICLOUD_APP_PASSWORD` 87 | 88 | ## 运行工具 89 | 90 | ### Windows 用户 91 | 92 | 双击可执行文件运行工具。 93 | 94 | ### Mac 用户 95 | 96 | 1. 打开终端 97 | 2. 导航到可执行文件所在的目录 98 | 3. `./CursorKeepAlive` 99 | 100 | ### 请按 `4` 开始自动化流程 101 | 102 | ## 免责声明 103 | 104 | 本项目仅为教育目的而创建。作者不对以下情况承担任何责任或义务: 105 | 106 | - 对代码或相关材料的任何滥用 107 | - 使用本项目产生的任何损害或法律后果 108 | - 所提供内容的准确性、完整性或实用性 109 | 110 | 使用本项目,即表示您同意风险自负。本项目不适用于生产环境,且不提供任何保证或担保。 111 | 如果您有任何法律或道德顾虑,请不要使用此存储库。 112 | 113 | ## 致谢 114 | 115 | 如果没有这些出色项目的帮助,本项目將无法完成: 116 | 117 | - [cursor-auto-free](https://github.com/chengazhen/cursor-auto-free) 118 | - [go-cursor-help](https://github.com/yuaotian/go-cursor-help) 119 | - [hidemyemail-generator](https://github.com/rtunazzz/hidemyemail-generator) 120 | 121 | ## 贡献 122 | 123 | 如果您想为本项目做出贡献,请随时提交拉取请求。 124 | 125 | ## 许可证 126 | 127 | 本产品根据专有许可证分发。您可以在以下链接查看完整的许可协议:[CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/)。 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cursor Pro (iCloud) Automation Tool - Free Trial Reset Tool 2 | 3 |
4 | 5 | [![Release](https://img.shields.io/github/v/release/ryan0204/cursor-auto-icloud?style=flat-square&logo=github&color=blue)](https://github.com/ryan0204/cursor-auto-icloud/releases/latest) 6 | [![Stars](https://img.shields.io/github/stars/ryan0204/cursor-auto-icloud?style=flat-square&logo=github)](https://github.com/ryan0204/cursor-auto-icloud/stargazers) 7 | 8 | 9 | ## ⭐️ Star us on GitHub — it motivates us a lot! 10 | 11 | [🌏 中文 README](README-zh.md) 12 | 13 | Cursor Logo 14 | 15 | Tool Preview 16 | 17 |
18 | 19 | ## Table of Contents 20 | 21 | - [Prepare](#prepare) 22 | - [Download](#download) 23 | - [Setting up](#setting-up) 24 | - [Running the tool](#running-the-tool) 25 | - [Disclaimer](#disclaimer) 26 | - [Credits](#credits) 27 | - [Contributing](#contributing) 28 | - [License](#license) 29 | 30 | ## Prepare 31 | 32 | You should have following items before using this tool: 33 | 34 | - An apple account (with @icloud.com as suffix) with **iCloud Plus** 35 | 36 | ## Download 37 | 38 | 1. Download the latest version from GitHub Releases 39 | 2. Choose the version according to your system: 40 | 41 | > Windows: Download CursorKeepAlive.exe directly 42 | > Mac (Intel): Choose x64 version 43 | > Mac (M series): Choose ARM64(aarch64) version 44 | 45 | ### Additional Steps for Mac Users 46 | 47 | > Open Terminal, navigate to the application directory 48 | > Execute the following command to make the file executable: 49 | > ```chmod +x ./CursorKeepAlive``` 50 | 51 | Follow the setup instructions below, then run the tool. 52 | 53 | ## Setting up 54 | 55 | ### Setting up environment variables 56 | 57 | > Mac User: If you are not able to rename the file, you can use `touch .env` to create the file in the same directory as executable file. 58 | 59 | 1. Download [`.env.example`](https://github.com/Ryan0204/cursor-auto-icloud/blob/main/.env.example) file and rename it to `.env` 60 | 2. Fill in the `.env` file 61 | 62 | ```env 63 | ICLOUD_USER=your_apple_id (!!! without @icloud.com) 64 | ICLOUD_APP_PASSWORD=your_apple_id_app_specific_password (explained below) 65 | ICLOUD_COOKIES=your_icloud_cookies (explained below) 66 | ``` 67 | 68 | ### Getting iCloud cookie string 69 | 70 | 1. Download [Cookie-Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) Chrome extension 71 | 2. Navigate to [iCloud settings](https://www.icloud.com/settings/) in your browser and log in 72 | 3. Click on the Cookie-Editor extension and export cookies with `Header String` format 73 | 4. Paste the exported cookies into a file named `.env` as `ICLOUD_COOKIES` 74 | 75 | Example Cookie: 76 | 77 | ``` 78 | X_APPLE_WEB_KB-V5590FJFX4ZYGBSJEZRZBTFB9UU=“xxxxxx”;X-APPLE-DS-WEB-SESSION-TOKEN=“xxxxxx”;X-APPLE-UNIQUE-CLIENT-ID=“xxxxxx”;X-APPLE-WEB-ID=28672BD9012631BA3CBAE022A1DBAEE2D0AFD358;X-APPLE-WEBAUTH-HSA-TRUST=“xxxxxx”;X-APPLE-WEBAUTH-LOGIN=“xxxxxx”;X-APPLE-WEBAUTH-PCS-Cloudkit=“xxxxxx”;X-APPLE-WEBAUTH-PCS-Documents=“xxxxxx”;X-APPLE-WEBAUTH-PCS-Mail=“xxxxxx”;X-APPLE-WEBAUTH-PCS-News=“xxxxxx”;X-APPLE-WEBAUTH-PCS-Notes=“xxxxxx”;X-APPLE-WEBAUTH-PCS-Photos=“xxxxxx”;X-APPLE-WEBAUTH-PCS-Safari=“xxxxxx”;X-APPLE-WEBAUTH-PCS-Sharing=“xxxxxx”;X-APPLE-WEBAUTH-TOKEN=“xxxxxx”;X-APPLE-WEBAUTH-USER=“xxxxxx”;X-APPLE-WEBAUTH-VALIDATE=“xxxxxx”; 79 | ``` 80 | 81 | ### Getting Apple ID App Specific Password 82 | 83 | 1. Sign in to your Apple Account on [account.apple.com](https://account.apple.com) 84 | 2. In the Sign-In and Security section, select App-Specific Passwords. 85 | 3. Select Generate an app-specific password, then follow the steps on your screen. 86 | 4. Copy the generated password and paste it into a file named `.env` as `ICLOUD_APP_PASSWORD` 87 | 88 | ## Running the tool 89 | 90 | ### Windows User 91 | 92 | Double-click the executable file to run the tool. 93 | 94 | ### Mac User 95 | 96 | 1. Open Terminal 97 | 2. Navigate to the directory where the executable file is located 98 | 3. `./CursorKeepAlive` 99 | 100 | ### Please press `4` to start the automation 101 | 102 | ## Disclaimer 103 | 104 | This project is created solely for educational purposes. The author(s) do not assume any responsibility or liability for: 105 | 106 | - Any misuse of the code or related materials. 107 | - Any damages or legal implications arising from the use of this project. 108 | - The accuracy, completeness, or usefulness of the provided content. 109 | 110 | By using this project, you agree that you are doing so at your own risk. This project is not intended for use in production environments, and no warranties or guarantees are provided. 111 | If you have any legal or ethical concerns, please refrain from using this repository. 112 | 113 | ## Credits 114 | 115 | This project can't be done without the help of these amazing projects: 116 | 117 | - [cursor-auto-free](https://github.com/chengazhen/cursor-auto-free) 118 | - [go-cursor-help](https://github.com/yuaotian/go-cursor-help) 119 | - [hidemyemail-generator](https://github.com/rtunazzz/hidemyemail-generator) 120 | 121 | ## Contributing 122 | 123 | If you want to contribute to this project, please feel free to open a pull request. 124 | 125 | ## License 126 | 127 | This product is distributed under a proprietary license. You can review the full license agreement at the following link: [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/). 128 | -------------------------------------------------------------------------------- /assets/img/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryan0204/cursor-auto-icloud/ea87ee415143cdd990dcb9069b315d6b887d432f/assets/img/preview.png -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set PYTHONWARNINGS=ignore::SyntaxWarning:DrissionPage 3 | echo Building Cursor Keep Alive... 4 | 5 | :: Check if virtual environment exists 6 | if not exist "venv" ( 7 | python -m venv venv 8 | if errorlevel 1 ( 9 | echo Failed to create virtual environment! 10 | exit /b 1 11 | ) 12 | ) 13 | 14 | :: Activate virtual environment and wait for activation to complete 15 | call venv\Scripts\activate.bat 16 | timeout /t 2 /nobreak > nul 17 | 18 | :: Install dependencies 19 | echo Installing dependencies... 20 | python -m pip install --upgrade pip 21 | pip install -r requirements.txt 22 | 23 | :: Run build script 24 | echo Starting build process... 25 | python build.py 26 | 27 | :: Deactivate virtual environment 28 | deactivate 29 | 30 | :: Keep window open 31 | echo Build completed! 32 | pause -------------------------------------------------------------------------------- /build.mac.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export PYTHONWARNINGS=ignore::SyntaxWarning:DrissionPage 3 | 4 | # Get script directory 5 | cd "$(dirname "$0")" 6 | 7 | echo "Creating virtual environment..." 8 | 9 | # Check if virtual environment exists 10 | if [ ! -d "venv" ]; then 11 | python3 -m venv venv 12 | if [ $? -ne 0 ]; then 13 | echo "Failed to create virtual environment!" 14 | exit 1 15 | fi 16 | fi 17 | 18 | # Activate virtual environment 19 | source venv/bin/activate 20 | 21 | # Install dependencies 22 | echo "Installing dependencies..." 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | 26 | # Run build script 27 | echo "Starting build process..." 28 | python build.py 29 | 30 | # Keep window open 31 | echo "Build completed!" 32 | echo "Press any key to exit..." 33 | read -n 1 -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | import os 3 | import platform 4 | import subprocess 5 | import time 6 | import threading 7 | import sys 8 | import shutil 9 | import argparse 10 | 11 | # Ensure UTF-8 encoding for console output 12 | if platform.system().lower() == "windows": 13 | # Force UTF-8 encoding on Windows 14 | import io 15 | sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') 16 | 17 | # Ignore specific SyntaxWarning 18 | warnings.filterwarnings("ignore", category=SyntaxWarning, module="DrissionPage") 19 | 20 | CURSOR_LOGO = """ 21 | ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ 22 | ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ 23 | ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ 24 | ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ 25 | ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ 26 | ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ 27 | """ 28 | 29 | # Update the paths to match the new directory structure 30 | MAIN_SCRIPT = "src/main.py" # New main entry point 31 | DATA_FOLDER = "data" 32 | CODE_FOLDER = "src" 33 | ENV_FILE = ".env" 34 | APP_NAME = "CursorKeepAlive" 35 | 36 | # Define platform-specific separator for PyInstaller 37 | SEPARATOR = ";" if platform.system().lower() == "windows" else ":" 38 | 39 | 40 | def get_platform_info(): 41 | """Get detailed platform information for build optimization""" 42 | system = platform.system().lower() 43 | arch = platform.machine().lower() 44 | 45 | # Determine if we're on Apple Silicon or Intel Mac 46 | if system == "darwin": 47 | if arch == "arm64": 48 | return system, "arm64", "mac_m1" 49 | else: 50 | return system, "x86_64", "mac_intel" 51 | elif system == "windows": 52 | return system, arch, "windows" 53 | elif system == "linux": 54 | return system, arch, "linux" 55 | else: 56 | return system, arch, system 57 | 58 | 59 | # Create recursive data file list for src directory 60 | def get_recursive_data_files(start_dir, target_dir): 61 | data_files = [] 62 | for root, dirs, files in os.walk(start_dir): 63 | # Skip __pycache__ directories 64 | if "__pycache__" in root: 65 | continue 66 | 67 | for file in files: 68 | if file.endswith(".py"): 69 | source = os.path.join(root, file) 70 | # Get the relative path within the src directory 71 | rel_path = os.path.relpath(source, start=os.path.dirname(start_dir)) 72 | # Set the target path 73 | target = os.path.dirname(os.path.join(target_dir, rel_path)) 74 | data_files.append((source, target)) 75 | return data_files 76 | 77 | class LoadingAnimation: 78 | def __init__(self): 79 | self.is_running = False 80 | self.animation_thread = None 81 | 82 | def start(self, message="Building"): 83 | self.is_running = True 84 | self.animation_thread = threading.Thread(target=self._animate, args=(message,)) 85 | self.animation_thread.start() 86 | 87 | def stop(self): 88 | self.is_running = False 89 | if self.animation_thread: 90 | self.animation_thread.join() 91 | print("\r" + " " * 70 + "\r", end="", flush=True) # Clear the line 92 | 93 | def _animate(self, message): 94 | animation = "|/-\\" 95 | idx = 0 96 | while self.is_running: 97 | print(f"\r{message} {animation[idx % len(animation)]}", end="", flush=True) 98 | idx += 1 99 | time.sleep(0.1) 100 | 101 | 102 | def print_logo(): 103 | # Check if running on Windows 104 | if platform.system().lower() == "windows": 105 | # Use a simpler ASCII version for Windows 106 | WINDOWS_LOGO = """ 107 | ______ 108 | / ____/_ __________ ____ _____ 109 | / / / / / / ___/ _ \/ __ \/ ___/ 110 | / /___/ /_/ / / / __/ /_/ / / 111 | \____/\__,_/_/ \___/\____/_/ 112 | """ 113 | print("\033[96m" + WINDOWS_LOGO + "\033[0m") 114 | else: 115 | # Use Unicode logo for other platforms 116 | print("\033[96m" + CURSOR_LOGO + "\033[0m") 117 | 118 | print("\033[93m" + "Building Cursor Keep Alive...".center(56) + "\033[0m\n") 119 | 120 | 121 | def progress_bar(progress, total, prefix="", length=50): 122 | filled = int(length * progress // total) 123 | bar = "█" * filled + "░" * (length - filled) 124 | percent = f"{100 * progress / total:.1f}" 125 | print(f"\r{prefix} |{bar}| {percent}% Complete", end="", flush=True) 126 | if progress == total: 127 | print() 128 | 129 | 130 | def simulate_progress(message, duration=1.0, steps=20): 131 | print(f"\033[94m{message}\033[0m") 132 | for i in range(steps + 1): 133 | time.sleep(duration / steps) 134 | progress_bar(i, steps, prefix="Progress:", length=40) 135 | 136 | 137 | def filter_output(output): 138 | """ImportantMessage""" 139 | if not output: 140 | return "" 141 | important_lines = [] 142 | for line in output.split("\n"): 143 | # Only keep lines containing specific keywords 144 | if any( 145 | keyword in line.lower() 146 | for keyword in ["error:", "failed:", "completed", "directory:"] 147 | ): 148 | important_lines.append(line) 149 | return "\n".join(important_lines) 150 | 151 | 152 | # Define additional data files to include 153 | base_data_files = [ 154 | (DATA_FOLDER, DATA_FOLDER), 155 | (ENV_FILE, "."), 156 | ("src/turnstilePatch", "src/turnstilePatch"), # Include turnstilePatch folder from src directory 157 | ("src/icloud", "src/icloud"), # Explicitly include the iCloud module directory 158 | ("names-dataset.txt", "."), # Include names dataset 159 | ] 160 | 161 | # Ensure data files exist and are accessible 162 | def ensure_files_exist(): 163 | # Create .env file if it doesn't exist 164 | if not os.path.exists(ENV_FILE) and os.path.exists(".env.example"): 165 | shutil.copy(".env.example", ENV_FILE) 166 | print(f"Created {ENV_FILE} from template") 167 | 168 | # Add .env file to data files 169 | if ENV_FILE not in [src for src, _ in base_data_files]: 170 | base_data_files.append((ENV_FILE, ".")) 171 | 172 | # Create empty accounts.csv file if it doesn't exist - in same directory as .env 173 | env_dir = os.path.dirname(os.path.abspath(ENV_FILE)) 174 | accounts_csv_path = os.path.join(env_dir, "accounts.csv") 175 | if not os.path.exists(accounts_csv_path): 176 | with open(accounts_csv_path, "w", newline="") as f: 177 | f.write("created_date,email,password,token,first_name,last_name\n") 178 | print(f"Created empty accounts.csv file at {accounts_csv_path}") 179 | 180 | # Add accounts.csv to data files (from same directory as .env) 181 | base_data_files.append((accounts_csv_path, ".")) 182 | 183 | # Create empty emails.txt file in same directory as .env 184 | emails_file_path = os.path.join(env_dir, "emails.txt") 185 | if not os.path.exists(emails_file_path): 186 | with open(emails_file_path, "w") as f: 187 | pass 188 | print(f"Created empty emails.txt file at {emails_file_path}") 189 | 190 | # Add emails.txt to data files (from same directory as .env) 191 | base_data_files.append((emails_file_path, ".")) 192 | 193 | # Ensure data directory exists 194 | if not os.path.exists(DATA_FOLDER): 195 | os.makedirs(DATA_FOLDER, exist_ok=True) 196 | 197 | 198 | def build(target_platform=None, target_arch=None): 199 | # Clear screen 200 | os.system("cls" if platform.system().lower() == "windows" else "clear") 201 | 202 | # Print logo 203 | print_logo() 204 | 205 | # Ensure necessary files exist 206 | ensure_files_exist() 207 | 208 | # Get platform information 209 | system, arch, platform_id = get_platform_info() 210 | 211 | # Override with target platform if provided 212 | if target_platform: 213 | platform_id = target_platform 214 | print(f"\033[93mBuilding for target platform: {platform_id}\033[0m") 215 | else: 216 | print(f"\033[93mBuilding for current platform: {platform_id} ({arch})\033[0m") 217 | 218 | # Set output directory based on platform 219 | output_dir = f"dist/{platform_id}" 220 | 221 | # Create output directory 222 | os.makedirs(output_dir, exist_ok=True) 223 | simulate_progress("Creating output directory...", 0.5) 224 | 225 | # Get Python source files 226 | py_data_files = get_recursive_data_files(CODE_FOLDER, "src") 227 | 228 | # Combine both lists 229 | data_files = base_data_files + py_data_files 230 | 231 | # Format data files for PyInstaller 232 | data_args = [] 233 | for src, dst in data_files: 234 | if os.path.exists(src): 235 | data_args.append(f"--add-data={src}{SEPARATOR}{dst}") 236 | 237 | # Base PyInstaller command 238 | command = [ 239 | "pyinstaller", 240 | "--onefile", 241 | "--clean", 242 | f"--name={APP_NAME}_{platform_id}" + (".exe" if platform_id == "windows" else ""), 243 | "--hidden-import=src", 244 | "--hidden-import=src.icloud", 245 | "--hidden-import=src.icloud.generateEmail", 246 | "--hidden-import=src.icloud.hidemyemail", 247 | "--hidden-import=src.core", 248 | "--hidden-import=src.utils", 249 | "--hidden-import=src.ui", 250 | "--hidden-import=src.auth", 251 | "--collect-submodules=src", 252 | ] 253 | 254 | # Add platform-specific options 255 | if target_arch: 256 | if system == "darwin": 257 | if target_arch == "universal2": 258 | command.append("--target-architecture=universal2") 259 | elif target_arch in ["x86_64", "arm64"]: 260 | command.append(f"--target-architecture={target_arch}") 261 | # Set specific environment variables for Intel builds 262 | if target_arch == "x86_64": 263 | os.environ["ARCHFLAGS"] = "-arch x86_64" 264 | os.environ["SYSTEM_VERSION_COMPAT"] = "1" 265 | os.environ["MACOSX_DEPLOYMENT_TARGET"] = "10.15" # Ensure compatibility 266 | print("Building for Intel: Set ARCHFLAGS=-arch x86_64 and SYSTEM_VERSION_COMPAT=1") 267 | 268 | # For macOS Intel builds, let's be more explicit 269 | if platform_id == "mac_intel": 270 | # Additional options for macOS Intel 271 | command.append("--clean") # Ensure clean build 272 | command.append("--noconfirm") # Don't ask for confirmation 273 | # Don't use --noconsole as it might cause issues with some app types 274 | # Don't use --osx-architecture flag as it's not supported in this PyInstaller version 275 | 276 | # Special handling for Intel builds on Apple Silicon 277 | if platform_id == "mac_intel" and arch == "arm64" and target_arch == "x86_64": 278 | print("\033[93mWarning: Building Intel binary on Apple Silicon - using cross-compilation\033[0m") 279 | 280 | # Check if this process is running under Rosetta 281 | is_rosetta = False 282 | try: 283 | result = subprocess.run(["sysctl", "-n", "sysctl.proc_translated"], 284 | capture_output=True, text=True, check=False) 285 | is_rosetta = (result.stdout.strip() == "1") 286 | except Exception: 287 | pass 288 | 289 | if not is_rosetta: 290 | print("\033[91mWarning: Not running under Rosetta/arch -x86_64. This may cause issues.\033[0m") 291 | print("\033[91mRecommendation: Run with 'arch -x86_64 python build.py --platform mac_intel --arch x86_64'\033[0m") 292 | 293 | # Add data files 294 | command.extend(data_args) 295 | 296 | # Add the main script 297 | command.append(MAIN_SCRIPT) 298 | 299 | loading = LoadingAnimation() 300 | try: 301 | simulate_progress(f"Running PyInstaller for {platform_id}...", 2.0) 302 | loading.start(f"Building for {platform_id} in progress") 303 | result = subprocess.run( 304 | command, check=True, capture_output=True, text=True 305 | ) 306 | loading.stop() 307 | 308 | if result.stderr: 309 | filtered_errors = [ 310 | line 311 | for line in result.stderr.split("\n") 312 | if any( 313 | keyword in line.lower() 314 | for keyword in ["error:", "failed:", "completed", "directory:"] 315 | ) 316 | ] 317 | if filtered_errors: 318 | print("\033[93mBuild Warnings/Errors:\033[0m") 319 | print("\n".join(filtered_errors)) 320 | 321 | except subprocess.CalledProcessError as e: 322 | loading.stop() 323 | print(f"\033[91mBuild failed with error code {e.returncode}\033[0m") 324 | if e.stderr: 325 | print("\033[91mError Details:\033[0m") 326 | print(e.stderr) 327 | return 328 | except FileNotFoundError: 329 | loading.stop() 330 | print( 331 | "\033[91mError: Please ensure PyInstaller is installed (pip install pyinstaller)\033[0m" 332 | ) 333 | return 334 | except KeyboardInterrupt: 335 | loading.stop() 336 | print("\n\033[91mBuild cancelled by user\033[0m") 337 | return 338 | finally: 339 | loading.stop() 340 | 341 | # Copy config file 342 | if os.path.exists("config.ini.example"): 343 | simulate_progress("Copying configuration file...", 0.5) 344 | if system == "windows": 345 | subprocess.run( 346 | ["copy", "config.ini.example", f"{output_dir}\\config.ini"], shell=True 347 | ) 348 | else: 349 | subprocess.run(["cp", "config.ini.example", f"{output_dir}/config.ini"]) 350 | 351 | # Copy .env.example file 352 | if os.path.exists(".env.example"): 353 | simulate_progress("Copying environment file...", 0.5) 354 | if system == "windows": 355 | subprocess.run(["copy", ".env.example", f"{output_dir}\\.env"], shell=True) 356 | else: 357 | subprocess.run(["cp", ".env.example", f"{output_dir}/.env"]) 358 | 359 | # Move the PyInstaller output to the platform-specific directory 360 | simulate_progress("Moving files to output directory...", 0.5) 361 | # PyInstaller typically puts files in a 'dist' folder in the root directory 362 | exec_name = f"{APP_NAME}_{platform_id}" + (".exe" if platform_id == "windows" else "") 363 | pyinstaller_output = os.path.join("dist", exec_name) 364 | 365 | # For macOS Intel builds, PyInstaller might use a different naming convention 366 | if platform_id == "mac_intel": 367 | # Try alternative output names that might be generated for Intel builds 368 | alt_names = [ 369 | os.path.join("dist", exec_name), # Standard name 370 | os.path.join("dist", f"{APP_NAME}"), # Base name without suffix 371 | os.path.join("dist", f"{APP_NAME}_macos"), # Generic macOS name 372 | os.path.join("dist", f"{APP_NAME}_darwin"), # Darwin name 373 | os.path.join("dist", f"{APP_NAME}_mac"), # Shortened mac name 374 | os.path.join("dist", "mac_intel", f"{APP_NAME}"), # In platform subfolder 375 | os.path.join("dist", "mac_intel", exec_name), # In platform subfolder with platform id 376 | ] 377 | 378 | # Find the first file that exists 379 | found_alt_name = False 380 | for alt_name in alt_names: 381 | if os.path.exists(alt_name): 382 | pyinstaller_output = alt_name 383 | print(f"Found Intel executable at alternative path: {alt_name}") 384 | found_alt_name = True 385 | break 386 | 387 | # If still not found, search for any executable in the dist directory 388 | if not found_alt_name: 389 | print("Searching for any executable in dist directory...") 390 | if os.path.exists("dist"): 391 | for root, dirs, files in os.walk("dist"): 392 | for file in files: 393 | # Skip known non-executable extensions 394 | if file.endswith((".py", ".pyc", ".pyo", ".spec", ".txt", ".log")): 395 | continue 396 | 397 | file_path = os.path.join(root, file) 398 | # Check if it's an executable (UNIX only) 399 | is_executable = False 400 | try: 401 | if system != "windows": 402 | is_executable = os.access(file_path, os.X_OK) 403 | else: 404 | # For Windows, check extension 405 | is_executable = file.endswith(".exe") 406 | except: 407 | pass 408 | 409 | # If it's executable or contains APP_NAME, try it 410 | if is_executable or APP_NAME.lower() in file.lower(): 411 | print(f"Found potential executable: {file_path}") 412 | pyinstaller_output = file_path 413 | found_alt_name = True 414 | break 415 | if found_alt_name: 416 | break 417 | 418 | # Create the output directory if it doesn't exist 419 | os.makedirs(output_dir, exist_ok=True) 420 | print(f"Created output directory: {output_dir}") 421 | 422 | # Check if the output file exists 423 | if os.path.exists(pyinstaller_output): 424 | try: 425 | # Get the destination file path 426 | dest_file = os.path.join(output_dir, exec_name) 427 | 428 | print(f"Copying from {pyinstaller_output} to {dest_file}") 429 | 430 | # Copy the file using shutil (more reliable cross-platform) 431 | shutil.copy2(pyinstaller_output, dest_file) 432 | 433 | # Verify the file exists in the destination 434 | if os.path.exists(dest_file): 435 | file_size = os.path.getsize(dest_file) 436 | print(f"SUCCESS: Copied file to {dest_file} (size: {file_size} bytes)") 437 | else: 438 | print(f"ERROR: Failed to verify file exists at {dest_file}") 439 | 440 | # Try creating an empty file for testing 441 | try: 442 | with open(os.path.join(output_dir, "test.txt"), "w") as f: 443 | f.write("Test file for debugging") 444 | print(f"Created test file at {os.path.join(output_dir, 'test.txt')}") 445 | except Exception as e: 446 | print(f"Failed to create test file: {str(e)}") 447 | except Exception as e: 448 | print(f"ERROR during copy operation: {str(e)}") 449 | else: 450 | print(f"ERROR: Source file not found at {pyinstaller_output}") 451 | 452 | # Look for similarly named files in the dist directory 453 | print("Looking for files in dist directory:") 454 | if os.path.exists("dist"): 455 | # Check all files in the dist directory 456 | for item in os.listdir("dist"): 457 | item_path = os.path.join("dist", item) 458 | if os.path.isfile(item_path): 459 | file_size = os.path.getsize(item_path) 460 | print(f" - Found file: {item} (size: {file_size} bytes)") 461 | 462 | # If the file name contains our app name, try copying it 463 | if APP_NAME.lower() in item.lower(): 464 | try: 465 | dest_file = os.path.join(output_dir, exec_name) 466 | shutil.copy2(item_path, dest_file) 467 | print(f"Copied alternative file {item} to {dest_file}") 468 | 469 | # Create a test file to verify write permissions 470 | with open(os.path.join(output_dir, "test.txt"), "w") as f: 471 | f.write("Test file for debugging") 472 | except Exception as e: 473 | print(f"Failed to copy alternative file: {str(e)}") 474 | elif os.path.isdir(item_path): 475 | print(f" - Found directory: {item}") 476 | # Check files in subdirectory 477 | for subitem in os.listdir(item_path): 478 | subitem_path = os.path.join(item_path, subitem) 479 | if os.path.isfile(subitem_path): 480 | print(f" - {subitem} (size: {os.path.getsize(subitem_path)} bytes)") 481 | 482 | # If it looks like our executable, copy it 483 | if APP_NAME.lower() in subitem.lower(): 484 | try: 485 | dest_file = os.path.join(output_dir, exec_name) 486 | shutil.copy2(subitem_path, dest_file) 487 | print(f"Copied file from subdirectory: {subitem} to {dest_file}") 488 | except Exception as e: 489 | print(f"Failed to copy from subdirectory: {str(e)}") 490 | else: 491 | print(" - dist directory not found") 492 | 493 | # Verify the contents of the output directory 494 | print(f"\nVerifying contents of {output_dir}:") 495 | if os.path.exists(output_dir): 496 | found_files = False 497 | for item in os.listdir(output_dir): 498 | found_files = True 499 | item_path = os.path.join(output_dir, item) 500 | if os.path.isfile(item_path): 501 | print(f" - File: {item} (size: {os.path.getsize(item_path)} bytes)") 502 | else: 503 | print(f" - Directory: {item}") 504 | 505 | if not found_files: 506 | print(" - Directory exists but is empty!") 507 | 508 | # As a last resort, create a dummy file so artifact upload doesn't fail 509 | try: 510 | with open(os.path.join(output_dir, "placeholder.txt"), "w") as f: 511 | f.write("Placeholder file created because no executable was found") 512 | print(" - Created placeholder.txt file") 513 | except Exception as e: 514 | print(f" - Failed to create placeholder: {str(e)}") 515 | else: 516 | print(" - Output directory does not exist!") 517 | 518 | # Print absolute paths for clarity 519 | print(f"\nAbsolute paths:") 520 | print(f"Current directory: {os.path.abspath(os.curdir)}") 521 | print(f"Output directory: {os.path.abspath(output_dir)}") 522 | if os.path.exists(pyinstaller_output): 523 | print(f"Source file: {os.path.abspath(pyinstaller_output)}") 524 | else: 525 | print(f"Source file not found: {os.path.abspath(pyinstaller_output)}") 526 | 527 | print( 528 | f"\n\033[92mBuild completed successfully! Output directory: {output_dir}\033[0m" 529 | ) 530 | 531 | 532 | def main(): 533 | parser = argparse.ArgumentParser(description="Build CursorKeepAlive for different platforms.") 534 | parser.add_argument("--platform", choices=["mac_m1", "mac_intel", "universal_mac", "windows", "linux", "all"], 535 | help="Target platform to build for") 536 | parser.add_argument("--arch", choices=["x86_64", "arm64", "universal2"], 537 | help="Target architecture (macOS only)") 538 | 539 | args = parser.parse_args() 540 | 541 | if args.platform == "all": 542 | if platform.system().lower() == "darwin": 543 | print("\033[95mBuilding for Mac M1...\033[0m") 544 | build("mac_m1", "arm64") 545 | 546 | print("\n\033[95mBuilding for Mac Intel...\033[0m") 547 | build("mac_intel", "x86_64") 548 | 549 | print("\n\033[95mBuilding for Universal Mac Binary...\033[0m") 550 | build("universal_mac", "universal2") 551 | elif platform.system().lower() == "windows": 552 | print("\033[95mBuilding for Windows...\033[0m") 553 | build("windows") 554 | elif platform.system().lower() == "linux": 555 | print("\033[95mBuilding for Linux...\033[0m") 556 | build("linux") 557 | else: 558 | print("\033[91mCan only build for all platforms when on macOS or Windows\033[0m") 559 | elif args.platform: 560 | build(args.platform, args.arch) 561 | else: 562 | build() 563 | 564 | 565 | if __name__ == "__main__": 566 | main() 567 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export PYTHONWARNINGS=ignore::SyntaxWarning:DrissionPage 3 | 4 | echo "Creating virtual environment..." 5 | 6 | # Check if virtual environment exists 7 | if [ ! -d "venv" ]; then 8 | python3 -m venv venv 9 | if [ $? -ne 0 ]; then 10 | echo "Failed to create virtual environment!" 11 | exit 1 12 | fi 13 | fi 14 | 15 | # Activate virtual environment 16 | source venv/bin/activate 17 | 18 | # Install dependencies 19 | echo "Installing dependencies..." 20 | python -m pip install --upgrade pip 21 | pip install -r requirements.txt 22 | 23 | # Run build script 24 | echo "Starting build process..." 25 | python build.py 26 | 27 | # Complete 28 | echo "Build completed!" -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is kept for backward compatibility. 3 | The actual implementation has moved to src/utils/config.py. 4 | """ 5 | 6 | import os 7 | import sys 8 | 9 | # Add the current directory to the path so we can import the src package 10 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 11 | 12 | # Import from the new location 13 | from src.utils.config import Config 14 | 15 | # Export the class for backward compatibility 16 | __all__ = ['Config'] 17 | -------------------------------------------------------------------------------- /data/emails.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryan0204/cursor-auto-icloud/ea87ee415143cdd990dcb9069b315d6b887d432f/data/emails.txt -------------------------------------------------------------------------------- /emails.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryan0204/cursor-auto-icloud/ea87ee415143cdd990dcb9069b315d6b887d432f/emails.txt -------------------------------------------------------------------------------- /generate_email.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Wrapper for iCloud Email Generator 4 | This provides an easy way to import and use the iCloud Hide My Email generator. 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | # Add the current directory to the path 11 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 12 | 13 | # Import from the new location 14 | try: 15 | from src.icloud.generateEmail import generateIcloudEmail 16 | except ImportError: 17 | # If that fails, try relative imports 18 | try: 19 | from src.icloud.generateEmail import generateIcloudEmail 20 | except ImportError: 21 | raise ImportError("Failed to import iCloud email generator. Make sure all dependencies are installed.") 22 | 23 | # Export the function for backward compatibility 24 | __all__ = ['generateIcloudEmail'] 25 | 26 | # If run directly, generate emails 27 | if __name__ == "__main__": 28 | import sys 29 | 30 | count = 5 # Default count 31 | if len(sys.argv) > 1: 32 | try: 33 | count = int(sys.argv[1]) 34 | except ValueError: 35 | print(f"Invalid count: {sys.argv[1]}") 36 | sys.exit(1) 37 | 38 | print(f"Generating {count} iCloud Hide My Email addresses...") 39 | emails = generateIcloudEmail(count) 40 | 41 | if emails: 42 | print(f"Successfully generated {len(emails)} email addresses:") 43 | for email in emails: 44 | print(email) 45 | else: 46 | print("Failed to generate any email addresses.") -------------------------------------------------------------------------------- /logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is kept for backward compatibility. 3 | """ 4 | 5 | import os 6 | import sys 7 | 8 | # Add the current directory to the path so we can import the src package 9 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 10 | 11 | # Import from the new location 12 | from src.utils.logger import * 13 | 14 | # No need to initialize anything here, as the src/utils/logger.py already initializes itself -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | DrissionPage==4.1.0.9 2 | colorama==0.4.6 3 | python-dotenv>=1.0.0 4 | pyinstaller 5 | requests 6 | aiohttp==3.9.4 7 | click==8.1.7 8 | certifi==2024.2.2 -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryan0204/cursor-auto-icloud/ea87ee415143cdd990dcb9069b315d6b887d432f/src/__init__.py -------------------------------------------------------------------------------- /src/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryan0204/cursor-auto-icloud/ea87ee415143cdd990dcb9069b315d6b887d432f/src/auth/__init__.py -------------------------------------------------------------------------------- /src/auth/cursor_auth_manager.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os 3 | import sys 4 | 5 | # Add parent directory to path to import language module 6 | parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 7 | if parent_dir not in sys.path: 8 | sys.path.append(parent_dir) 9 | 10 | try: 11 | from src.utils.language import getTranslation, _ 12 | except ImportError: 13 | from utils.language import getTranslation, _ 14 | 15 | 16 | class CursorAuthManager: 17 | """Cursor认证信息管理器""" 18 | 19 | def __init__(self): 20 | # 判断操作系统 21 | if sys.platform == "win32": # Windows 22 | appdata = os.getenv("APPDATA") 23 | if appdata is None: 24 | raise EnvironmentError(getTranslation("appdata_not_set")) 25 | self.db_path = os.path.join( 26 | appdata, "Cursor", "User", "globalStorage", "state.vscdb" 27 | ) 28 | elif sys.platform == "darwin": # macOS 29 | self.db_path = os.path.abspath(os.path.expanduser( 30 | "~/Library/Application Support/Cursor/User/globalStorage/state.vscdb" 31 | )) 32 | elif sys.platform == "linux" : # Linux 和其他类Unix系统 33 | self.db_path = os.path.abspath(os.path.expanduser( 34 | "~/.config/Cursor/User/globalStorage/state.vscdb" 35 | )) 36 | else: 37 | raise NotImplementedError(getTranslation("unsupported_os").format(sys.platform)) 38 | 39 | def update_auth(self, email=None, access_token=None, refresh_token=None): 40 | """ 41 | 更新Cursor的认证信息 42 | :param email: 新的邮箱地址 43 | :param access_token: 新的访问令牌 44 | :param refresh_token: 新的刷新令牌 45 | :return: bool 是否成功更新 46 | """ 47 | updates = [] 48 | # 登录状态 49 | updates.append(("cursorAuth/cachedSignUpType", "Auth_0")) 50 | 51 | if email is not None: 52 | updates.append(("cursorAuth/cachedEmail", email)) 53 | if access_token is not None: 54 | updates.append(("cursorAuth/accessToken", refresh_token)) 55 | if refresh_token is not None: 56 | updates.append(("cursorAuth/refreshToken", refresh_token)) 57 | 58 | if not updates: 59 | print(getTranslation("no_values_to_update")) 60 | return False 61 | 62 | conn = None 63 | try: 64 | conn = sqlite3.connect(self.db_path) 65 | cursor = conn.cursor() 66 | 67 | for key, value in updates: 68 | 69 | # 如果没有更新任何行,说明key不存在,执行插入 70 | # 检查 accessToken 是否存在 71 | check_query = f"SELECT COUNT(*) FROM itemTable WHERE key = ?" 72 | cursor.execute(check_query, (key,)) 73 | if cursor.fetchone()[0] == 0: 74 | insert_query = "INSERT INTO itemTable (key, value) VALUES (?, ?)" 75 | cursor.execute(insert_query, (key, value)) 76 | else: 77 | update_query = "UPDATE itemTable SET value = ? WHERE key = ?" 78 | cursor.execute(update_query, (value, key)) 79 | 80 | if cursor.rowcount > 0: 81 | print(getTranslation("value_updated_success").format(key.split('/')[-1])) 82 | else: 83 | print(getTranslation("value_not_found_or_unchanged").format(key.split('/')[-1])) 84 | 85 | conn.commit() 86 | return True 87 | 88 | except sqlite3.Error as e: 89 | print(getTranslation("database_error").format(str(e))) 90 | return False 91 | except Exception as e: 92 | print(getTranslation("general_error").format(str(e))) 93 | return False 94 | finally: 95 | if conn: 96 | conn.close() 97 | -------------------------------------------------------------------------------- /src/auth/patch_cursor_get_machine_id.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | import logging 6 | import os 7 | import platform 8 | import re 9 | import shutil 10 | import sys 11 | import tempfile 12 | from typing import Tuple 13 | 14 | # Add parent directory to path to import language module 15 | parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 16 | if parent_dir not in sys.path: 17 | sys.path.append(parent_dir) 18 | 19 | try: 20 | from src.utils.language import getTranslation, _ 21 | except ImportError: 22 | try: 23 | from utils.language import getTranslation, _ 24 | except ImportError: 25 | # Fallback if language module is not available 26 | def getTranslation(key, *args): 27 | if args: 28 | return key.format(*args) 29 | return key 30 | 31 | 32 | # 配置日志 33 | def setup_logging() -> logging.Logger: 34 | """配置并返回logger实例""" 35 | logger = logging.getLogger(__name__) 36 | logger.setLevel(logging.INFO) 37 | handler = logging.StreamHandler() 38 | formatter = logging.Formatter( 39 | "%(asctime)s - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S" 40 | ) 41 | handler.setFormatter(formatter) 42 | logger.addHandler(handler) 43 | return logger 44 | 45 | 46 | logger = setup_logging() 47 | 48 | 49 | def get_cursor_paths() -> Tuple[str, str]: 50 | """ 51 | 根据不同操作系统获取 Cursor 相关路径 52 | 53 | Returns: 54 | Tuple[str, str]: (package.json路径, main.js路径)的元组 55 | 56 | Raises: 57 | OSError: 当找不到有效路径或系统不支持时抛出 58 | """ 59 | system = platform.system() 60 | 61 | paths_map = { 62 | "Darwin": { 63 | "base": "/Applications/Cursor.app/Contents/Resources/app", 64 | "package": "package.json", 65 | "main": "out/main.js", 66 | }, 67 | "Windows": { 68 | "base": os.path.join( 69 | os.getenv("USERAPPPATH") or os.path.join(os.getenv("LOCALAPPDATA", ""), "Programs", "Cursor", "resources", "app") 70 | ), 71 | "package": "package.json", 72 | "main": "out/main.js", 73 | }, 74 | "Linux": { 75 | "bases": ["/opt/Cursor/resources/app", "/usr/share/cursor/resources/app"], 76 | "package": "package.json", 77 | "main": "out/main.js", 78 | }, 79 | } 80 | 81 | if system not in paths_map: 82 | raise OSError(getTranslation("unsupported_os").format(system)) 83 | 84 | if system == "Linux": 85 | for base in paths_map["Linux"]["bases"]: 86 | pkg_path = os.path.join(base, paths_map["Linux"]["package"]) 87 | if os.path.exists(pkg_path): 88 | return (pkg_path, os.path.join(base, paths_map["Linux"]["main"])) 89 | raise OSError(getTranslation("cursor_path_not_found_linux")) 90 | 91 | base_path = paths_map[system]["base"] 92 | # 判断Windows是否存在这个文件夹,如果不存在,提示需要创建软连接后重试 93 | if system == "Windows": 94 | if not os.path.exists(base_path): 95 | logging.info(getTranslation("cursor_path_not_default")) 96 | logging.info(getTranslation("create_symlink_command")) 97 | logging.info(getTranslation("example_command")) 98 | logging.info(getTranslation("example_command_path")) 99 | input(getTranslation("press_enter_exit")) 100 | return ( 101 | os.path.join(base_path, paths_map[system]["package"]), 102 | os.path.join(base_path, paths_map[system]["main"]), 103 | ) 104 | 105 | 106 | def check_system_requirements(pkg_path: str, main_path: str) -> bool: 107 | """ 108 | 检查系统要求 109 | 110 | Args: 111 | pkg_path: package.json 文件路径 112 | main_path: main.js 文件路径 113 | 114 | Returns: 115 | bool: 检查是否通过 116 | """ 117 | for file_path in [pkg_path, main_path]: 118 | if not os.path.isfile(file_path): 119 | logger.error(getTranslation("file_not_exist").format(file_path)) 120 | return False 121 | 122 | if not os.access(file_path, os.W_OK): 123 | logger.error(getTranslation("file_no_write_permission").format(file_path)) 124 | return False 125 | 126 | return True 127 | 128 | 129 | def version_check(version: str, min_version: str = "", max_version: str = "") -> bool: 130 | """ 131 | 版本号检查 132 | 133 | Args: 134 | version: 当前版本号 135 | min_version: 最小版本号要求 136 | max_version: 最大版本号要求 137 | 138 | Returns: 139 | bool: 版本号是否符合要求 140 | """ 141 | version_pattern = r"^\d+\.\d+\.\d+$" 142 | try: 143 | if not re.match(version_pattern, version): 144 | logger.error(getTranslation("invalid_version_format").format(version)) 145 | return False 146 | 147 | def parse_version(ver: str) -> Tuple[int, ...]: 148 | return tuple(map(int, ver.split("."))) 149 | 150 | current = parse_version(version) 151 | 152 | if min_version and current < parse_version(min_version): 153 | logger.error(getTranslation("version_below_minimum").format(version, min_version)) 154 | return False 155 | 156 | if max_version and current > parse_version(max_version): 157 | logger.error(getTranslation("version_above_maximum").format(version, max_version)) 158 | return False 159 | 160 | return True 161 | 162 | except Exception as e: 163 | logger.error(getTranslation("version_check_failed").format(str(e))) 164 | return False 165 | 166 | 167 | def modify_main_js(main_path: str) -> bool: 168 | """ 169 | 修改 main.js 文件 170 | 171 | Args: 172 | main_path: main.js 文件路径 173 | 174 | Returns: 175 | bool: 修改是否成功 176 | """ 177 | try: 178 | # 获取原始文件的权限和所有者信息 179 | original_stat = os.stat(main_path) 180 | original_mode = original_stat.st_mode 181 | original_uid = original_stat.st_uid 182 | original_gid = original_stat.st_gid 183 | 184 | with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp_file: 185 | with open(main_path, "r", encoding="utf-8") as main_file: 186 | content = main_file.read() 187 | 188 | # 执行替换 189 | patterns = { 190 | r"async getMachineId\(\)\{return [^??]+\?\?([^}]+)\}": r"async getMachineId(){return \1}", 191 | r"async getMacMachineId\(\)\{return [^??]+\?\?([^}]+)\}": r"async getMacMachineId(){return \1}", 192 | } 193 | 194 | for pattern, replacement in patterns.items(): 195 | content = re.sub(pattern, replacement, content) 196 | 197 | tmp_file.write(content) 198 | tmp_path = tmp_file.name 199 | 200 | # 使用 shutil.copy2 保留文件权限 201 | shutil.copy2(main_path, main_path + ".old") 202 | shutil.move(tmp_path, main_path) 203 | 204 | # 恢复原始文件的权限和所有者 205 | os.chmod(main_path, original_mode) 206 | if os.name != "nt": # 在非Windows系统上设置所有者 207 | os.chown(main_path, original_uid, original_gid) 208 | 209 | logger.info(getTranslation("file_modified_success")) 210 | return True 211 | 212 | except Exception as e: 213 | logger.error(getTranslation("file_modification_error").format(str(e))) 214 | if "tmp_path" in locals(): 215 | os.unlink(tmp_path) 216 | return False 217 | 218 | 219 | def backup_files(pkg_path: str, main_path: str) -> bool: 220 | """ 221 | 备份原始文件 222 | 223 | Args: 224 | pkg_path: package.json 文件路径(未使用) 225 | main_path: main.js 文件路径 226 | 227 | Returns: 228 | bool: 备份是否成功 229 | """ 230 | try: 231 | # 只备份 main.js 232 | if os.path.exists(main_path): 233 | backup_main = f"{main_path}.bak" 234 | shutil.copy2(main_path, backup_main) 235 | logger.info(getTranslation("mainjs_backup_created").format(backup_main)) 236 | 237 | return True 238 | except Exception as e: 239 | logger.error(getTranslation("backup_failed").format(str(e))) 240 | return False 241 | 242 | 243 | def restore_backup_files(pkg_path: str, main_path: str) -> bool: 244 | """ 245 | 恢复备份文件 246 | 247 | Args: 248 | pkg_path: package.json 文件路径(未使用) 249 | main_path: main.js 文件路径 250 | 251 | Returns: 252 | bool: 恢复是否成功 253 | """ 254 | try: 255 | # 只恢复 main.js 256 | backup_main = f"{main_path}.bak" 257 | if os.path.exists(backup_main): 258 | shutil.copy2(backup_main, main_path) 259 | logger.info(getTranslation("mainjs_restored")) 260 | return True 261 | 262 | logger.error(getTranslation("backup_not_found")) 263 | return False 264 | except Exception as e: 265 | logger.error(getTranslation("restore_backup_failed").format(str(e))) 266 | return False 267 | 268 | 269 | def patch_cursor_get_machine_id(restore_mode=False) -> None: 270 | """ 271 | 主函数 272 | 273 | Args: 274 | restore_mode: 是否为恢复模式 275 | """ 276 | logger.info(getTranslation("script_execution_started")) 277 | 278 | try: 279 | # 获取路径 280 | pkg_path, main_path = get_cursor_paths() 281 | 282 | # 检查系统要求 283 | if not check_system_requirements(pkg_path, main_path): 284 | sys.exit(1) 285 | 286 | if restore_mode: 287 | # 恢复备份 288 | if restore_backup_files(pkg_path, main_path): 289 | logger.info(getTranslation("backup_restore_complete")) 290 | else: 291 | logger.error(getTranslation("backup_restore_failed")) 292 | return 293 | 294 | # 获取版本号 295 | try: 296 | with open(pkg_path, "r", encoding="utf-8") as f: 297 | version = json.load(f)["version"] 298 | logger.info(getTranslation("current_cursor_version").format(version)) 299 | except Exception as e: 300 | logger.error(getTranslation("reading_version_failed").format(str(e))) 301 | sys.exit(1) 302 | 303 | # 检查版本 304 | if not version_check(version, min_version="0.45.0"): 305 | logger.error(getTranslation("version_not_supported")) 306 | sys.exit(1) 307 | 308 | logger.info(getTranslation("version_check_passed")) 309 | 310 | # 备份文件 311 | if not backup_files(pkg_path, main_path): 312 | logger.error(getTranslation("backup_failed_abort")) 313 | sys.exit(1) 314 | 315 | # 修改文件 316 | if not modify_main_js(main_path): 317 | sys.exit(1) 318 | 319 | logger.info(getTranslation("script_execution_complete")) 320 | 321 | except Exception as e: 322 | logger.error(getTranslation("execution_error").format(str(e))) 323 | sys.exit(1) 324 | 325 | 326 | if __name__ == "__main__": 327 | patch_cursor_get_machine_id() 328 | -------------------------------------------------------------------------------- /src/auth/reset_machine.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import uuid 5 | import hashlib 6 | import shutil 7 | from colorama import Fore, Style, init 8 | 9 | # Add parent directory to path to import language module 10 | parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 11 | if parent_dir not in sys.path: 12 | sys.path.append(parent_dir) 13 | 14 | try: 15 | from src.utils.language import getTranslation, _ 16 | except ImportError: 17 | from utils.language import getTranslation, _ 18 | 19 | # 初始化colorama 20 | init() 21 | 22 | # 定义emoji和颜色常量 23 | EMOJI = { 24 | "FILE": "📄", 25 | "BACKUP": "💾", 26 | "SUCCESS": "✅", 27 | "ERROR": "❌", 28 | "INFO": "ℹ️", 29 | "RESET": "🔄", 30 | } 31 | 32 | 33 | class MachineIDResetter: 34 | def __init__(self): 35 | # 判断操作系统 36 | if sys.platform == "win32": # Windows 37 | appdata = os.getenv("APPDATA") 38 | if appdata is None: 39 | raise EnvironmentError(getTranslation("appdata_not_set")) 40 | self.db_path = os.path.join( 41 | appdata, "Cursor", "User", "globalStorage", "storage.json" 42 | ) 43 | elif sys.platform == "darwin": # macOS 44 | self.db_path = os.path.abspath( 45 | os.path.expanduser( 46 | "~/Library/Application Support/Cursor/User/globalStorage/storage.json" 47 | ) 48 | ) 49 | elif sys.platform == "linux": # Linux 和其他类Unix系统 50 | self.db_path = os.path.abspath( 51 | os.path.expanduser("~/.config/Cursor/User/globalStorage/storage.json") 52 | ) 53 | else: 54 | raise NotImplementedError(getTranslation("unsupported_os").format(sys.platform)) 55 | 56 | def generate_new_ids(self): 57 | """生成新的机器ID""" 58 | # 生成新的UUID 59 | dev_device_id = str(uuid.uuid4()) 60 | 61 | # 生成新的machineId (64个字符的十六进制) 62 | machine_id = hashlib.sha256(os.urandom(32)).hexdigest() 63 | 64 | # 生成新的macMachineId (128个字符的十六进制) 65 | mac_machine_id = hashlib.sha512(os.urandom(64)).hexdigest() 66 | 67 | # 生成新的sqmId 68 | sqm_id = "{" + str(uuid.uuid4()).upper() + "}" 69 | 70 | return { 71 | "telemetry.devDeviceId": dev_device_id, 72 | "telemetry.macMachineId": mac_machine_id, 73 | "telemetry.machineId": machine_id, 74 | "telemetry.sqmId": sqm_id, 75 | } 76 | 77 | def reset_machine_ids(self): 78 | """重置机器ID并备份原文件""" 79 | try: 80 | print(f"{Fore.CYAN}{EMOJI['INFO']} {getTranslation('checking_config_file')}...{Style.RESET_ALL}") 81 | 82 | # 检查文件是否存在 83 | if not os.path.exists(self.db_path): 84 | print( 85 | f"{Fore.RED}{EMOJI['ERROR']} {getTranslation('config_file_not_exist')}: {self.db_path}{Style.RESET_ALL}" 86 | ) 87 | return False 88 | 89 | # 检查文件权限 90 | if not os.access(self.db_path, os.R_OK | os.W_OK): 91 | print( 92 | f"{Fore.RED}{EMOJI['ERROR']} {getTranslation('config_file_no_permission')}{Style.RESET_ALL}" 93 | ) 94 | print( 95 | f"{Fore.RED}{EMOJI['ERROR']} {getTranslation('go_cursor_help_warning')} {self.db_path} {Style.RESET_ALL}" 96 | ) 97 | return False 98 | 99 | # 读取现有配置 100 | print(f"{Fore.CYAN}{EMOJI['FILE']} {getTranslation('reading_current_config')}...{Style.RESET_ALL}") 101 | with open(self.db_path, "r", encoding="utf-8") as f: 102 | config = json.load(f) 103 | 104 | # 生成新的ID 105 | print(f"{Fore.CYAN}{EMOJI['RESET']} {getTranslation('generating_new_machine_ids')}...{Style.RESET_ALL}") 106 | new_ids = self.generate_new_ids() 107 | 108 | # 更新配置 109 | config.update(new_ids) 110 | 111 | # 保存新配置 112 | print(f"{Fore.CYAN}{EMOJI['FILE']} {getTranslation('saving_new_config')}...{Style.RESET_ALL}") 113 | with open(self.db_path, "w", encoding="utf-8") as f: 114 | json.dump(config, f, indent=4) 115 | 116 | print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {getTranslation('machine_id_reset_success')}{Style.RESET_ALL}") 117 | print(f"\n{Fore.CYAN}{getTranslation('new_machine_ids')}:{Style.RESET_ALL}") 118 | for key, value in new_ids.items(): 119 | print(f"{EMOJI['INFO']} {key}: {Fore.GREEN}{value}{Style.RESET_ALL}") 120 | 121 | return True 122 | 123 | except PermissionError as e: 124 | print(f"{Fore.RED}{EMOJI['ERROR']} {getTranslation('permission_error')}: {str(e)}{Style.RESET_ALL}") 125 | print( 126 | f"{Fore.YELLOW}{EMOJI['INFO']} {getTranslation('run_as_admin_suggestion')}{Style.RESET_ALL}" 127 | ) 128 | return False 129 | except Exception as e: 130 | print(f"{Fore.RED}{EMOJI['ERROR']} {getTranslation('reset_process_error')}: {str(e)}{Style.RESET_ALL}") 131 | 132 | return False 133 | 134 | 135 | if __name__ == "__main__": 136 | print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}") 137 | print(f"{Fore.CYAN}{EMOJI['RESET']} {getTranslation('cursor_machine_id_reset_tool')}{Style.RESET_ALL}") 138 | print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}") 139 | 140 | resetter = MachineIDResetter() 141 | resetter.reset_machine_ids() 142 | 143 | print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}") 144 | input(f"{EMOJI['INFO']} {getTranslation('press_enter_exit')}...") 145 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryan0204/cursor-auto-icloud/ea87ee415143cdd990dcb9069b315d6b887d432f/src/core/__init__.py -------------------------------------------------------------------------------- /src/core/cursor_pro_keep_alive.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import json 4 | import sys 5 | import csv 6 | import uuid 7 | import secrets 8 | import hashlib 9 | import base64 10 | from pathlib import Path 11 | import dotenv 12 | import requests 13 | 14 | from enum import Enum 15 | from typing import Optional, Tuple 16 | 17 | from src.core.exit_cursor import ExitCursor 18 | import src.core.go_cursor_help as go_cursor_help 19 | import src.auth.patch_cursor_get_machine_id as patch_cursor_get_machine_id 20 | from src.auth.reset_machine import MachineIDResetter 21 | 22 | from src.icloud.generateEmail import generateIcloudEmail 23 | from src.icloud.deleteEmail import deleteIcloudEmail 24 | 25 | os.environ["PYTHONVERBOSE"] = "0" 26 | os.environ["PYINSTALLER_VERBOSE"] = "0" 27 | 28 | import time 29 | import random 30 | from src.auth.cursor_auth_manager import CursorAuthManager 31 | import os 32 | from src.utils.logger import logging 33 | from src.utils.browser_utils import BrowserManager 34 | from src.utils.get_email_code import EmailVerificationHandler 35 | from src.ui.logo import print_logo 36 | from src.utils.config import Config 37 | from src.utils.language import LanguageManager, Language, _, getTranslation 38 | from datetime import datetime 39 | 40 | # 定义 EMOJI 字典 41 | EMOJI = {"ERROR": "❌", "WARNING": "⚠️", "INFO": "ℹ️"} 42 | 43 | 44 | class VerificationStatus(Enum): 45 | """验证状态枚举""" 46 | 47 | PASSWORD_PAGE = "@name=password" 48 | CAPTCHA_PAGE = "@data-index=0" 49 | ACCOUNT_SETTINGS = "Account Settings" 50 | 51 | 52 | class TurnstileError(Exception): 53 | """Turnstile 验证相关异常""" 54 | 55 | pass 56 | 57 | 58 | def save_screenshot(tab, stage: str, timestamp: bool = True) -> None: 59 | """ 60 | 保存页面截图 61 | 62 | Args: 63 | tab: 浏览器标签页对象 64 | stage: 截图阶段标识 65 | timestamp: 是否添加时间戳 66 | """ 67 | try: 68 | # 创建 screenshots 目录 69 | screenshot_dir = "screenshots" 70 | if not os.path.exists(screenshot_dir): 71 | os.makedirs(screenshot_dir) 72 | 73 | # 生成文件名 74 | if timestamp: 75 | filename = f"turnstile_{stage}_{int(time.time())}.png" 76 | else: 77 | filename = f"turnstile_{stage}.png" 78 | 79 | filepath = os.path.join(screenshot_dir, filename) 80 | 81 | # 保存截图 82 | tab.get_screenshot(filepath) 83 | logging.debug(getTranslation("screenshot_saved").format(filepath)) 84 | except Exception as e: 85 | logging.warning(getTranslation("screenshot_save_failed").format(str(e))) 86 | 87 | 88 | def check_verification_success(tab) -> Optional[VerificationStatus]: 89 | """ 90 | 检查验证是否成功 91 | 92 | Returns: 93 | VerificationStatus: 验证成功时返回对应状态,失败返回 None 94 | """ 95 | for status in VerificationStatus: 96 | if tab.ele(status.value): 97 | logging.info(getTranslation("verification_success_page").format(status.name)) 98 | return status 99 | return None 100 | 101 | 102 | def handle_turnstile(tab, max_retries: int = 2, retry_interval: tuple = (1, 2)) -> bool: 103 | """ 104 | 处理 Turnstile 验证 105 | 106 | Args: 107 | tab: 浏览器标签页对象 108 | max_retries: 最大重试次数 109 | retry_interval: 重试间隔时间范围(最小值, 最大值) 110 | 111 | Returns: 112 | bool: 验证是否成功 113 | 114 | Raises: 115 | TurnstileError: 验证过程中出现异常 116 | """ 117 | logging.info(getTranslation("detecting_turnstile")) 118 | save_screenshot(tab, "start") 119 | 120 | retry_count = 0 121 | 122 | try: 123 | while retry_count < max_retries: 124 | retry_count += 1 125 | logging.debug(getTranslation("verification_attempt").format(retry_count)) 126 | 127 | try: 128 | # 定位验证框元素 129 | challenge_check = ( 130 | tab.ele("@id=cf-turnstile", timeout=2) 131 | .child() 132 | .shadow_root.ele("tag:iframe") 133 | .ele("tag:body") 134 | .sr("tag:input") 135 | ) 136 | 137 | if challenge_check: 138 | logging.info(getTranslation("turnstile_detected")) 139 | # 随机延时后点击验证 140 | time.sleep(random.uniform(1, 3)) 141 | challenge_check.click() 142 | time.sleep(2) 143 | 144 | # 保存验证后的截图 145 | save_screenshot(tab, "clicked") 146 | 147 | # 检查验证结果 148 | if check_verification_success(tab): 149 | logging.info(getTranslation("turnstile_passed")) 150 | save_screenshot(tab, "success") 151 | return True 152 | 153 | except Exception as e: 154 | logging.debug(getTranslation("attempt_failed").format(str(e))) 155 | 156 | # 检查是否已经验证成功 157 | if check_verification_success(tab): 158 | return True 159 | 160 | # 随机延时后继续下一次尝试 161 | time.sleep(random.uniform(*retry_interval)) 162 | 163 | # 超出最大重试次数 164 | logging.error(getTranslation("verification_max_retries_reached").format(max_retries)) 165 | logging.error(getTranslation("visit_project_for_info")) 166 | save_screenshot(tab, "failed") 167 | return False 168 | 169 | except Exception as e: 170 | error_msg = getTranslation("turnstile_exception").format(str(e)) 171 | logging.error(error_msg) 172 | save_screenshot(tab, "error") 173 | raise TurnstileError(error_msg) 174 | 175 | 176 | 177 | def get_cursor_session_token(tab, max_attempts: int = 3, retry_interval: int = 2) -> Optional[Tuple[str, str]]: 178 | """ 179 | 获取Cursor会话token 180 | 181 | Args: 182 | tab: 浏览器标签页对象 183 | max_attempts: 最大尝试次数 184 | retry_interval: 重试间隔(秒) 185 | 186 | Returns: 187 | Tuple[str, str] | None: 成功返回(userId, accessToken)元组,失败返回None 188 | """ 189 | logging.info(getTranslation("start_getting_session_token")) 190 | 191 | # 首先尝试使用UUID深度登录方式 192 | logging.info(getTranslation("try_deep_login")) 193 | 194 | def _generate_pkce_pair(): 195 | """生成PKCE验证对""" 196 | code_verifier = secrets.token_urlsafe(43) 197 | code_challenge_digest = hashlib.sha256(code_verifier.encode('utf-8')).digest() 198 | code_challenge = base64.urlsafe_b64encode(code_challenge_digest).decode('utf-8').rstrip('=') 199 | return code_verifier, code_challenge 200 | 201 | attempts = 0 202 | while attempts < max_attempts: 203 | try: 204 | verifier, challenge = _generate_pkce_pair() 205 | id = uuid.uuid4() 206 | client_login_url = f"https://www.cursor.com/cn/loginDeepControl?challenge={challenge}&uuid={id}&mode=login" 207 | 208 | logging.info(getTranslation("visiting_deep_login_url").format(client_login_url)) 209 | tab.get(client_login_url) 210 | save_screenshot(tab, f"deeplogin_attempt_{attempts}") 211 | 212 | if tab.ele("xpath=//span[contains(text(), 'Yes, Log In')]", timeout=5): 213 | logging.info(getTranslation("clicking_confirm_login")) 214 | tab.ele("xpath=//span[contains(text(), 'Yes, Log In')]").click() 215 | time.sleep(1.5) 216 | 217 | auth_poll_url = f"https://api2.cursor.sh/auth/poll?uuid={id}&verifier={verifier}" 218 | headers = { 219 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/0.48.6 Chrome/132.0.6834.210 Electron/34.3.4 Safari/537.36", 220 | "Accept": "*/*" 221 | } 222 | 223 | logging.info(getTranslation("polling_auth_status").format(auth_poll_url)) 224 | response = requests.get(auth_poll_url, headers=headers, timeout=5) 225 | 226 | if response.status_code == 200: 227 | data = response.json() 228 | accessToken = data.get("accessToken", None) 229 | authId = data.get("authId", "") 230 | 231 | if accessToken: 232 | userId = "" 233 | if len(authId.split("|")) > 1: 234 | userId = authId.split("|")[1] 235 | 236 | logging.info(getTranslation("token_userid_success")) 237 | return userId, accessToken 238 | else: 239 | logging.error(getTranslation("api_request_failed").format(response.status_code)) 240 | else: 241 | logging.warning(getTranslation("login_confirm_button_not_found")) 242 | 243 | attempts += 1 244 | if attempts < max_attempts: 245 | wait_time = retry_interval * attempts # 逐步增加等待时间 246 | logging.warning(getTranslation("token_attempt_failed").format(attempts, wait_time)) 247 | save_screenshot(tab, f"token_attempt_{attempts}") 248 | time.sleep(wait_time) 249 | 250 | except Exception as e: 251 | logging.error(getTranslation("deep_login_token_failed").format(str(e))) 252 | attempts += 1 253 | save_screenshot(tab, f"token_error_{attempts}") 254 | if attempts < max_attempts: 255 | wait_time = retry_interval * attempts 256 | logging.warning(getTranslation("retry_in_seconds").format(wait_time)) 257 | time.sleep(wait_time) 258 | 259 | # 所有尝试都失败后返回None 260 | logging.error(getTranslation("max_attempts_reached").format(max_attempts)) 261 | return None 262 | 263 | 264 | def update_cursor_auth(email=None, access_token=None, refresh_token=None): 265 | """ 266 | 更新Cursor的认证信息的便捷函数 267 | """ 268 | auth_manager = CursorAuthManager() 269 | return auth_manager.update_auth(email, access_token, refresh_token) 270 | 271 | 272 | def sign_up_account(browser, tab, sign_up_url, settings_url, first_name, last_name, account, password, email_handler): 273 | """ 274 | Handle the account sign-up process 275 | 276 | Args: 277 | browser: Browser instance 278 | tab: Browser tab 279 | sign_up_url: URL for the signup page 280 | settings_url: URL for the settings page 281 | first_name: First name for the account 282 | last_name: Last name for the account 283 | account: Email account 284 | password: Password for the account 285 | email_handler: Email verification handler 286 | 287 | Returns: 288 | bool: True if signup was successful, False otherwise 289 | """ 290 | logging.info(getTranslation("start_registration")) 291 | logging.info(getTranslation("visiting_login_page").format(sign_up_url)) 292 | tab.get(sign_up_url) 293 | 294 | try: 295 | if tab.ele("@name=first_name"): 296 | logging.info(getTranslation("filling_personal_info")) 297 | tab.actions.click("@name=first_name").input(first_name) 298 | logging.info(getTranslation("input_first_name").format(first_name)) 299 | time.sleep(random.uniform(1, 3)) 300 | 301 | tab.actions.click("@name=last_name").input(last_name) 302 | logging.info(getTranslation("input_last_name").format(last_name)) 303 | time.sleep(random.uniform(1, 3)) 304 | 305 | tab.actions.click("@name=email").input(account) 306 | logging.info(getTranslation("input_email").format(account)) 307 | time.sleep(random.uniform(1, 3)) 308 | 309 | logging.info(getTranslation("submit_personal_info")) 310 | tab.actions.click("@type=submit") 311 | 312 | except Exception as e: 313 | logging.error(getTranslation("signup_page_access_failed").format(str(e))) 314 | return False 315 | 316 | handle_turnstile(tab) 317 | 318 | try: 319 | if tab.ele("@name=password"): 320 | logging.info(getTranslation("setting_password")) 321 | tab.ele("@name=password").input(password) 322 | time.sleep(random.uniform(1, 3)) 323 | 324 | logging.info(getTranslation("submit_password")) 325 | tab.ele("@type=submit").click() 326 | logging.info(getTranslation("password_setup_complete")) 327 | 328 | except Exception as e: 329 | logging.error(getTranslation("password_setup_failed").format(str(e))) 330 | return False 331 | 332 | if tab.ele("This email is not available."): 333 | logging.error(getTranslation("email_already_used")) 334 | return False 335 | 336 | handle_turnstile(tab) 337 | 338 | while True: 339 | try: 340 | if tab.ele("Account Settings"): 341 | logging.info(getTranslation("registration_successful")) 342 | break 343 | if tab.ele("@data-index=0"): 344 | logging.info(getTranslation("getting_verification_code")) 345 | code = email_handler.get_verification_code() 346 | if not code: 347 | logging.error(getTranslation("verification_code_failed")) 348 | return False 349 | 350 | logging.info(getTranslation("verification_code_success").format(code)) 351 | logging.info(getTranslation("entering_verification_code")) 352 | i = 0 353 | for digit in code: 354 | tab.ele(f"@data-index={i}").input(digit) 355 | time.sleep(random.uniform(0.1, 0.3)) 356 | i += 1 357 | logging.info(getTranslation("verification_code_complete")) 358 | break 359 | except Exception as e: 360 | logging.error(getTranslation("verification_process_error").format(str(e))) 361 | 362 | handle_turnstile(tab) 363 | wait_time = random.randint(3, 6) 364 | for i in range(wait_time): 365 | logging.info(getTranslation("waiting_for_processing").format(wait_time-i)) 366 | time.sleep(1) 367 | 368 | logging.info(getTranslation("getting_account_info")) 369 | tab.get(settings_url) 370 | try: 371 | usage_selector = ( 372 | "css:div.col-span-2 > div > div > div > div > " 373 | "div:nth-child(1) > div.flex.items-center.justify-between.gap-2 > " 374 | "span.font-mono.text-sm\\/\\[0\\.875rem\\]" 375 | ) 376 | usage_ele = tab.ele(usage_selector) 377 | if usage_ele: 378 | usage_info = usage_ele.text 379 | total_usage = usage_info.split("/")[-1].strip() 380 | logging.info(getTranslation("account_usage_limit").format(total_usage)) 381 | logging.info(getTranslation("visit_project_for_info")) 382 | 383 | except Exception as e: 384 | logging.error(getTranslation("get_account_limit_failed").format(str(e))) 385 | 386 | logging.info(getTranslation("registration_complete")) 387 | account_info = getTranslation("cursor_account_info").format(account, password) 388 | logging.info(account_info) 389 | time.sleep(5) 390 | return True 391 | 392 | 393 | def get_env_directory(): 394 | """ 395 | Get the directory where the .env file is located. 396 | Returns the directory path if .env exists, otherwise returns the current directory. 397 | """ 398 | # Check common locations for .env file 399 | possible_env_paths = [ 400 | ".env", # Current directory 401 | os.path.join(os.path.dirname(sys.executable), ".env"), # Next to executable 402 | os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), ".env") # Project root 403 | ] 404 | 405 | for env_path in possible_env_paths: 406 | if os.path.exists(env_path): 407 | return os.path.dirname(os.path.abspath(env_path)) 408 | 409 | # If .env is not found, return current directory 410 | return os.path.abspath(".") 411 | 412 | 413 | class EmailGenerator: 414 | def __init__( 415 | self, 416 | password="".join( 417 | random.choices( 418 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*", 419 | k=12, 420 | ) 421 | ), 422 | use_icloud=False, 423 | delete_after_use=False 424 | ): 425 | configInstance = Config() 426 | configInstance.print_config() 427 | self.names = self.load_names() 428 | self.default_password = password 429 | self.default_first_name = self.generate_random_name() 430 | self.default_last_name = self.generate_random_name() 431 | self.use_icloud = use_icloud 432 | self.delete_after_use = delete_after_use 433 | self.generated_email = None 434 | self.generateIcloudEmail = None 435 | self.deleteIcloudEmail = None 436 | 437 | # Try to load dotenv config if exists 438 | try: 439 | dotenv.load_dotenv() 440 | except Exception as e: 441 | logging.warning(getTranslation("env_file_load_failed").format(str(e))) 442 | 443 | # Try to import iCloud email generator if use_icloud is True 444 | if self.use_icloud: 445 | try: 446 | # Import the modules from the correct location 447 | from src.icloud.generateEmail import generateIcloudEmail 448 | from src.icloud.deleteEmail import deleteIcloudEmail 449 | self.generateIcloudEmail = generateIcloudEmail 450 | self.deleteIcloudEmail = deleteIcloudEmail 451 | logging.info(getTranslation("icloud_feature_enabled")) 452 | except ImportError: 453 | try: 454 | # Try relative import as fallback 455 | current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 456 | if current_dir not in sys.path: 457 | sys.path.append(current_dir) 458 | from icloud.generateEmail import generateIcloudEmail 459 | from icloud.deleteEmail import deleteIcloudEmail 460 | self.generateIcloudEmail = generateIcloudEmail 461 | self.deleteIcloudEmail = deleteIcloudEmail 462 | logging.info(getTranslation("icloud_feature_enabled")) 463 | except ImportError: 464 | logging.error(getTranslation("icloud_module_import_failed_local")) 465 | self.use_icloud = False 466 | 467 | def load_names(self): 468 | """Load names from names-dataset.txt file""" 469 | # Look for the file in the executable directory first, then in the project structure 470 | possible_paths = [ 471 | "names-dataset.txt", # In the current/executable directory 472 | os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 473 | "data", "names-dataset.txt") # Project structure path 474 | ] 475 | 476 | for names_file_path in possible_paths: 477 | try: 478 | with open(names_file_path, "r") as file: 479 | logging.info(getTranslation("names_dataset_loaded").format(names_file_path)) 480 | return file.read().split() 481 | except FileNotFoundError: 482 | continue 483 | 484 | logging.error(getTranslation("names_dataset_not_found")) 485 | # Return a small set of default names as fallback 486 | return ["John", "Jane", "Michael", "Emma", "Robert", "Olivia"] 487 | 488 | def generate_random_name(self): 489 | """生成随机用户名""" 490 | return random.choice(self.names) 491 | 492 | def get_emails_file_path(self): 493 | """Get the path to the emails.txt file, prioritizing accessible locations""" 494 | # Check if EMAIL_FILE_PATH is defined in .env 495 | env_path = os.environ.get("EMAIL_FILE_PATH") 496 | if env_path and os.path.exists(env_path): 497 | return env_path 498 | 499 | # First try to place emails.txt in the same directory as .env 500 | env_dir = get_env_directory() 501 | env_dir_path = os.path.join(env_dir, "emails.txt") 502 | 503 | # Try common locations 504 | possible_paths = [ 505 | env_dir_path, # Same directory as .env 506 | "data/emails.txt", # In the current/executable directory 507 | os.path.join(os.path.dirname(sys.executable), "data", "emails.txt"), # Next to executable 508 | os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 509 | "data", "emails.txt") # Project structure path 510 | ] 511 | 512 | for path in possible_paths: 513 | if os.path.exists(path): 514 | return path 515 | 516 | # Default to the same location as .env file 517 | default_path = env_dir_path 518 | try: 519 | # Make sure the directory exists 520 | os.makedirs(os.path.dirname(default_path), exist_ok=True) 521 | except: 522 | # If creating directory fails, use data directory as fallback 523 | default_path = "data/emails.txt" 524 | os.makedirs(os.path.dirname(default_path), exist_ok=True) 525 | 526 | return default_path 527 | 528 | def generate_email(self, length=4): 529 | """ 530 | 生成随机邮箱地址,如果启用了 iCloud 功能则使用 iCloud 隐藏邮箱 531 | """ 532 | # If iCloud is enabled, try to generate an iCloud email 533 | if self.use_icloud: 534 | try: 535 | emails = self.generateIcloudEmail(1, True) 536 | if emails and len(emails) > 0: 537 | self.generated_email = emails[0] 538 | return self.generated_email 539 | else: 540 | logging.warning(getTranslation("icloud_email_gen_failed")) 541 | except Exception as e: 542 | logging.error(getTranslation("icloud_email_gen_error").format(str(e))) 543 | logging.warning(getTranslation("using_local_email_list")) 544 | 545 | # If iCloud failed or not enabled, use local email list 546 | emails_file_path = self.get_emails_file_path() 547 | logging.info(f"Using emails file: {emails_file_path}") 548 | 549 | # Ensure the data directory exists 550 | os.makedirs(os.path.dirname(emails_file_path), exist_ok=True) 551 | 552 | # Check if emails.txt exists and has content 553 | try: 554 | if not os.path.exists(emails_file_path): 555 | with open(emails_file_path, "w") as f: 556 | pass 557 | logging.warning(getTranslation("empty_email_file_created").format(emails_file_path)) 558 | 559 | with open(emails_file_path, "r") as f: 560 | lines = f.readlines() 561 | 562 | if not lines: 563 | logging.warning(getTranslation("email_list_empty")) 564 | sys.exit(1) 565 | 566 | first_email = lines[0].strip() 567 | self.generated_email = first_email 568 | 569 | # Write remaining emails back to file 570 | with open(emails_file_path, "w") as f: 571 | f.writelines(lines[1:]) 572 | 573 | return self.generated_email 574 | except Exception as e: 575 | logging.error(getTranslation("email_file_read_error").format(str(e))) 576 | logging.warning(getTranslation("email_list_empty")) 577 | sys.exit(1) 578 | 579 | def delete_generated_email(self): 580 | """ 581 | Delete the generated iCloud email if delete_after_use is enabled 582 | 583 | Returns: 584 | bool: True if deletion was successful or not needed, False otherwise 585 | """ 586 | if not self.use_icloud or not self.delete_after_use or not self.generated_email: 587 | return True 588 | 589 | if not self.deleteIcloudEmail: 590 | logging.warning(getTranslation("delete_email_not_available")) 591 | return False 592 | 593 | try: 594 | logging.info(getTranslation("deleting_generated_email").format(self.generated_email)) 595 | results = self.deleteIcloudEmail(self.generated_email) 596 | 597 | if results and len(results) > 0: 598 | email, success, message = results[0] 599 | if success: 600 | logging.info(message) 601 | return True 602 | else: 603 | logging.error(message) 604 | return False 605 | else: 606 | logging.error(getTranslation("delete_email_no_result")) 607 | return False 608 | except Exception as e: 609 | logging.error(getTranslation("delete_email_exception").format(str(e))) 610 | return False 611 | 612 | def get_account_info(self): 613 | """获取完整的账号信息""" 614 | return { 615 | "email": self.generate_email(), 616 | "password": self.default_password, 617 | "first_name": self.default_first_name, 618 | "last_name": self.default_last_name, 619 | } 620 | 621 | 622 | def get_user_agent(): 623 | """获取user_agent""" 624 | try: 625 | # 使用JavaScript获取user agent 626 | browser_manager = BrowserManager() 627 | browser = browser_manager.init_browser() 628 | user_agent = browser.latest_tab.run_js("return navigator.userAgent") 629 | browser_manager.quit() 630 | return user_agent 631 | except Exception as e: 632 | logging.error(getTranslation("get_user_agent_failed").format(str(e))) 633 | return None 634 | 635 | 636 | def check_cursor_version(): 637 | """检查cursor版本""" 638 | pkg_path, main_path = patch_cursor_get_machine_id.get_cursor_paths() 639 | with open(pkg_path, "r", encoding="utf-8") as f: 640 | version = json.load(f)["version"] 641 | return patch_cursor_get_machine_id.version_check(version, min_version="0.45.0") 642 | 643 | 644 | def reset_machine_id(greater_than_0_45): 645 | if greater_than_0_45: 646 | # 提示请手动执行脚本 https://github.com/Ryan0204/cursor-auto-icloud/blob/main/patch_cursor_get_machine_id.py 647 | go_cursor_help.go_cursor_help() 648 | else: 649 | MachineIDResetter().reset_machine_ids() 650 | 651 | 652 | def print_end_message(): 653 | logging.info(getTranslation("operation_complete")) 654 | 655 | 656 | def save_account_to_csv(account_info, csv_path="accounts.csv"): 657 | """ 658 | Save account information to a CSV file. 659 | 660 | Args: 661 | account_info: Dictionary containing account details 662 | csv_path: Path to the CSV file 663 | """ 664 | # Check for CSV_FILE_PATH in environment variables 665 | env_csv_path = os.environ.get("CSV_FILE_PATH") 666 | if env_csv_path: 667 | csv_path = env_csv_path 668 | else: 669 | # Try to save accounts.csv in the same directory as .env 670 | env_dir = get_env_directory() 671 | csv_path = os.path.join(env_dir, "accounts.csv") 672 | 673 | file_path = Path(csv_path) 674 | logging.info(getTranslation("saving_account_to_csv").format(file_path)) 675 | 676 | # Check if file exists to determine if we need to write headers 677 | file_exists = file_path.exists() 678 | 679 | try: 680 | # Ensure the directory exists 681 | os.makedirs(os.path.dirname(os.path.abspath(csv_path)), exist_ok=True) 682 | 683 | with open(file_path, mode='a', newline='') as file: 684 | fieldnames = ['created_date', 'email', 'password', 'access_token', 'refresh_token', 'first_name', 'last_name'] 685 | writer = csv.DictWriter(file, fieldnames=fieldnames) 686 | 687 | # Write headers if file doesn't exist 688 | if not file_exists: 689 | writer.writeheader() 690 | 691 | # Add creation date to account info 692 | account_info_with_date = account_info.copy() 693 | account_info_with_date['created_date'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 694 | 695 | # Write account info 696 | writer.writerow(account_info_with_date) 697 | 698 | logging.info(getTranslation("account_saved_to_csv").format(csv_path)) 699 | return True 700 | except Exception as e: 701 | logging.error(getTranslation("save_account_failed").format(str(e))) 702 | return False 703 | 704 | 705 | def main(): 706 | """Main function for the Cursor Pro Keep Alive application.""" 707 | greater_than_0_45 = check_cursor_version() 708 | browser_manager = None 709 | 710 | # Initialize the language manager 711 | lang_manager = LanguageManager() 712 | 713 | # Define URLs used in the program 714 | login_url = "https://authenticator.cursor.sh" 715 | sign_up_url = "https://authenticator.cursor.sh/sign-up" 716 | settings_url = "https://www.cursor.com/settings" 717 | 718 | try: 719 | logging.info(getTranslation("program_init")) 720 | ExitCursor() 721 | 722 | # Main menu loop to handle language switching 723 | while True: 724 | # Using the new getTranslation function for more readable code 725 | print(getTranslation("select_operation_mode")) 726 | print(getTranslation("reset_machine_code_only")) 727 | print(getTranslation("complete_registration")) 728 | print(getTranslation("generate_icloud_email")) 729 | print(getTranslation("complete_registration_icloud")) 730 | print(getTranslation("select_language")) 731 | 732 | while True: 733 | try: 734 | # Using the original _ function for comparison 735 | choice = int(input(_("enter_option")).strip()) 736 | if choice in [1, 2, 3, 4, 5]: 737 | break 738 | else: 739 | print(_("invalid_option")) 740 | except ValueError: 741 | print(_("enter_valid_number")) 742 | 743 | if choice == 5: 744 | # Switch language 745 | lang_manager.select_language() 746 | continue # Return to the main menu with new language 747 | else: 748 | break # Exit the menu loop and proceed with the selected option 749 | 750 | # Set delete_icloud_email_after_use based on user choice if using iCloud 751 | delete_icloud_email_after_use = False 752 | if choice == 4: # If using iCloud email 753 | # Ask user if they want to delete the email after use 754 | delete_prompt = input(getTranslation("delete_email_prompt") + " (Y/N) [Y]: ").strip().upper() 755 | # Default is Yes (empty or Y) 756 | delete_icloud_email_after_use = delete_prompt != "N" 757 | if delete_icloud_email_after_use: 758 | logging.info(getTranslation("delete_after_use_enabled")) 759 | else: 760 | logging.info(getTranslation("delete_after_use_disabled")) 761 | 762 | if choice == 1: 763 | # 仅执行重置机器码 764 | reset_machine_id(greater_than_0_45) 765 | logging.info(getTranslation("reset_complete")) 766 | print_end_message() 767 | sys.exit(0) 768 | 769 | elif choice == 3: 770 | # 生成 iCloud 隐藏邮箱 771 | try: 772 | count = int(input(getTranslation("enter_email_count")).strip()) 773 | if count <= 0: 774 | logging.error(getTranslation("email_count_gt_zero")) 775 | sys.exit(1) 776 | 777 | # Import the iCloud email generator 778 | try: 779 | # Try direct import first 780 | from src.icloud.generateEmail import generateIcloudEmail 781 | emails = generateIcloudEmail(count) 782 | except ImportError: 783 | try: 784 | # Try with modified path as fallback 785 | current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 786 | if current_dir not in sys.path: 787 | sys.path.append(current_dir) 788 | from icloud.generateEmail import generateIcloudEmail 789 | emails = generateIcloudEmail(count) 790 | except ImportError: 791 | logging.error(getTranslation("icloud_module_import_failed")) 792 | print(getTranslation("install_dependencies")) 793 | print_end_message() 794 | sys.exit(1) 795 | 796 | if emails: 797 | print(getTranslation("generated_emails").format(len(emails))) 798 | for email in emails: 799 | print(email) 800 | else: 801 | print(getTranslation("no_emails_generated")) 802 | except ValueError: 803 | logging.error(getTranslation("invalid_count")) 804 | sys.exit(1) 805 | 806 | logging.info(getTranslation("initializing_browser")) 807 | 808 | # 获取user_agent 809 | user_agent = get_user_agent() 810 | if not user_agent: 811 | logging.error(getTranslation("getting_user_agent_failed")) 812 | user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" 813 | 814 | # 剔除user_agent中的"HeadlessChrome" 815 | user_agent = user_agent.replace("HeadlessChrome", "Chrome") 816 | 817 | browser_manager = BrowserManager() 818 | browser = browser_manager.init_browser(user_agent) 819 | 820 | # 获取并打印浏览器的user-agent 821 | user_agent = browser.latest_tab.run_js("return navigator.userAgent") 822 | 823 | logging.info(getTranslation("visit_project_for_info")) 824 | logging.info(getTranslation("config_info")) 825 | 826 | logging.info(getTranslation("generating_random_account")) 827 | 828 | # 使用 iCloud 隐藏邮箱 if choice is 4 829 | use_icloud = (choice == 4) 830 | email_generator = EmailGenerator(use_icloud=use_icloud, delete_after_use=delete_icloud_email_after_use) 831 | first_name = email_generator.default_first_name 832 | last_name = email_generator.default_last_name 833 | account = email_generator.generate_email() 834 | password = email_generator.default_password 835 | 836 | logging.info(getTranslation("generated_email_account").format(account)) 837 | 838 | logging.info(getTranslation("initializing_email_verification")) 839 | email_handler = EmailVerificationHandler(account) 840 | 841 | auto_update_cursor_auth = True 842 | 843 | tab = browser.latest_tab 844 | 845 | tab.run_js("try { turnstile.reset() } catch(e) { }") 846 | 847 | logging.info(getTranslation("start_registration")) 848 | logging.info(getTranslation("visiting_login_page").format(login_url)) 849 | tab.get(login_url) 850 | 851 | if sign_up_account(browser, tab, sign_up_url, settings_url, first_name, last_name, account, password, email_handler): 852 | logging.info(getTranslation("getting_session_token")) 853 | access_token, refresh_token = get_cursor_session_token(tab) 854 | if access_token and refresh_token: 855 | account_info = { 856 | 'email': account, 857 | 'password': password, 858 | 'access_token': access_token, 859 | 'refresh_token': refresh_token, 860 | 'first_name': first_name, 861 | 'last_name': last_name 862 | } 863 | save_account_to_csv(account_info) 864 | logging.info(getTranslation("updating_auth_info")) 865 | update_cursor_auth( 866 | email=account, access_token=access_token, refresh_token=refresh_token 867 | ) 868 | 869 | # Delete iCloud email if option is enabled 870 | if use_icloud and delete_icloud_email_after_use: 871 | logging.info(getTranslation("deleting_icloud_email_after_use")) 872 | email_generator.delete_generated_email() 873 | 874 | logging.info(getTranslation("visit_project_for_info")) 875 | logging.info(getTranslation("resetting_machine_code")) 876 | reset_machine_id(greater_than_0_45) 877 | logging.info(getTranslation("all_operations_complete")) 878 | print_end_message() 879 | else: 880 | logging.error(getTranslation("session_token_failed")) 881 | 882 | except Exception as e: 883 | logging.error(getTranslation("program_execution_error").format(str(e))) 884 | import traceback 885 | 886 | logging.error(traceback.format_exc()) 887 | finally: 888 | if browser_manager: 889 | browser_manager.quit() 890 | input(getTranslation("program_complete")) 891 | 892 | 893 | # If this script is run directly, execute the main function 894 | if __name__ == "__main__": 895 | main() 896 | -------------------------------------------------------------------------------- /src/core/exit_cursor.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | import time 3 | import os 4 | import sys 5 | 6 | # Add parent directory to path to import language module 7 | parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 8 | if parent_dir not in sys.path: 9 | sys.path.append(parent_dir) 10 | 11 | try: 12 | from src.utils.logger import logging 13 | from src.utils.language import getTranslation, _ 14 | except ImportError: 15 | from logger import logging 16 | try: 17 | from utils.language import getTranslation, _ 18 | except ImportError: 19 | # Fallback if language module is not available 20 | def getTranslation(key, *args): 21 | if args: 22 | return key.format(*args) 23 | return key 24 | 25 | def ExitCursor(timeout=5): 26 | """ 27 | 温和地关闭 Cursor 进程 28 | 29 | Args: 30 | timeout (int): 等待进程自然终止的超时时间(秒) 31 | Returns: 32 | bool: 是否成功关闭所有进程 33 | """ 34 | try: 35 | logging.info(getTranslation("starting_cursor_exit")) 36 | cursor_processes = [] 37 | # 收集所有 Cursor 进程 38 | for proc in psutil.process_iter(['pid', 'name']): 39 | try: 40 | if proc.info['name'].lower() in ['cursor.exe', 'cursor']: 41 | cursor_processes.append(proc) 42 | except (psutil.NoSuchProcess, psutil.AccessDenied): 43 | continue 44 | 45 | if not cursor_processes: 46 | logging.info(getTranslation("no_cursor_processes_found")) 47 | return True 48 | 49 | # 温和地请求进程终止 50 | for proc in cursor_processes: 51 | try: 52 | if proc.is_running(): 53 | proc.terminate() # 发送终止信号 54 | except (psutil.NoSuchProcess, psutil.AccessDenied): 55 | continue 56 | 57 | # 等待进程自然终止 58 | start_time = time.time() 59 | while time.time() - start_time < timeout: 60 | still_running = [] 61 | for proc in cursor_processes: 62 | try: 63 | if proc.is_running(): 64 | still_running.append(proc) 65 | except (psutil.NoSuchProcess, psutil.AccessDenied): 66 | continue 67 | 68 | if not still_running: 69 | logging.info(getTranslation("all_cursor_processes_closed")) 70 | return True 71 | 72 | # 等待一小段时间再检查 73 | time.sleep(0.5) 74 | 75 | # 如果超时后仍有进程在运行 76 | if still_running: 77 | process_list = ", ".join([str(p.pid) for p in still_running]) 78 | logging.warning(getTranslation("processes_not_closed_in_time").format(process_list)) 79 | return False 80 | 81 | return True 82 | 83 | except Exception as e: 84 | logging.error(getTranslation("error_closing_cursor").format(str(e))) 85 | return False 86 | 87 | if __name__ == "__main__": 88 | ExitCursor() 89 | -------------------------------------------------------------------------------- /src/core/go_cursor_help.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | # Add parent directory to path to import language module 7 | parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 8 | if parent_dir not in sys.path: 9 | sys.path.append(parent_dir) 10 | 11 | try: 12 | from src.utils.logger import logging 13 | from src.utils.language import getTranslation, _ 14 | except ImportError: 15 | from logger import logging 16 | try: 17 | from utils.language import getTranslation, _ 18 | except ImportError: 19 | # Fallback if language module is not available 20 | def getTranslation(key, *args): 21 | return key 22 | 23 | def go_cursor_help(): 24 | system = platform.system() 25 | logging.info(getTranslation("current_operating_system").format(system)) 26 | 27 | base_url = "https://aizaozao.com/accelerate.php/https://raw.githubusercontent.com/yuaotian/go-cursor-help/refs/heads/master/scripts/run" 28 | 29 | if system == "Darwin": # macOS 30 | cmd = f'curl -fsSL {base_url}/cursor_mac_id_modifier.sh | sudo bash' 31 | logging.info(getTranslation("executing_macos_command")) 32 | os.system(cmd) 33 | elif system == "Linux": 34 | cmd = f'curl -fsSL {base_url}/cursor_linux_id_modifier.sh | sudo bash' 35 | logging.info(getTranslation("executing_linux_command")) 36 | os.system(cmd) 37 | elif system == "Windows": 38 | cmd = f'irm {base_url}/cursor_win_id_modifier.ps1 | iex' 39 | logging.info(getTranslation("executing_windows_command")) 40 | # 在Windows上使用PowerShell执行命令 41 | subprocess.run(["powershell", "-Command", cmd], shell=True) 42 | else: 43 | logging.error(getTranslation("unsupported_os").format(system)) 44 | return False 45 | 46 | return True 47 | -------------------------------------------------------------------------------- /src/icloud/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | iCloud module for generating Hide My Email addresses. 3 | """ 4 | 5 | from src.icloud.generateEmail import generateIcloudEmail 6 | from src.icloud.hidemyemail import HideMyEmail 7 | 8 | __all__ = ["generateIcloudEmail", "HideMyEmail"] -------------------------------------------------------------------------------- /src/icloud/deleteEmail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | iCloud Email Deletion Utility 4 | This module deletes Hide My Email addresses from iCloud accounts. 5 | """ 6 | 7 | import os 8 | import sys 9 | import asyncio 10 | from typing import List, Optional, Union, Tuple 11 | 12 | # Add parent directory to path to import language module 13 | parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 14 | if parent_dir not in sys.path: 15 | sys.path.append(parent_dir) 16 | 17 | try: 18 | from src.utils.logger import logging 19 | from src.utils.config import Config 20 | from src.icloud.hidemyemail import HideMyEmail 21 | from src.utils.language import getTranslation, _ 22 | except ImportError: 23 | from utils.logger import logging 24 | from utils.config import Config 25 | from icloud.hidemyemail import HideMyEmail 26 | from utils.language import getTranslation, _ 27 | 28 | async def _delete_email(cookies: str, email: str) -> Tuple[bool, str]: 29 | """ 30 | Delete a single Hide My Email address from iCloud 31 | 32 | Args: 33 | cookies: iCloud cookies for authentication 34 | email: Email address to delete 35 | 36 | Returns: 37 | Tuple[bool, str]: (Success status, Message) 38 | """ 39 | try: 40 | logging.info(getTranslation("deleting_email").format(email)) 41 | 42 | # Use with statement for proper resource management since the class requires it 43 | async with HideMyEmail(label="Cursor-Auto-iCloud", cookies=cookies) as hide_my_email: 44 | # Get the list of existing emails 45 | email_list_response = await hide_my_email.list_email() 46 | if not email_list_response or not email_list_response.get("success", False): 47 | return False, getTranslation("no_emails_found") 48 | 49 | # Extract emails from the response - emails are in result.hmeEmails 50 | email_list = email_list_response.get("result", {}).get("hmeEmails", []) 51 | 52 | # Find the email in the list 53 | found_email = None 54 | for e in email_list: 55 | if e.get('hme') == email: 56 | found_email = e 57 | break 58 | 59 | if not found_email: 60 | return False, getTranslation("email_not_found").format(email) 61 | 62 | # First deactivate the email using the anonymousId 63 | anonymous_id = found_email.get('anonymousId') 64 | if not anonymous_id: 65 | return False, getTranslation("email_missing_id").format(email) 66 | 67 | deactivate_result = await hide_my_email.deactivate_email(anonymous_id) 68 | if not deactivate_result or not deactivate_result.get("success", False): 69 | reason = deactivate_result.get("reason", getTranslation("unknown_error")) if deactivate_result else getTranslation("unknown_error") 70 | return False, getTranslation("email_deactivation_failed").format(reason) 71 | 72 | # Then delete the email using the anonymousId 73 | delete_result = await hide_my_email.delete_email(anonymous_id, email) 74 | 75 | if delete_result and delete_result.get("success", False): 76 | return True, getTranslation("email_deleted_success").format(email) 77 | else: 78 | reason = delete_result.get("reason", getTranslation("unknown_error")) if delete_result else getTranslation("unknown_error") 79 | return False, getTranslation("email_deletion_failed").format(reason) 80 | 81 | except asyncio.TimeoutError: 82 | return False, getTranslation("delete_email_timeout") 83 | except Exception as e: 84 | logging.error(f"Exception details: {str(e)}") 85 | import traceback 86 | logging.error(traceback.format_exc()) 87 | return False, getTranslation("delete_email_failed").format(str(e)) 88 | 89 | def deleteIcloudEmail(email: Union[str, List[str]]) -> List[Tuple[str, bool, str]]: 90 | """ 91 | Delete iCloud Hide My Email address(es) 92 | 93 | Args: 94 | email: Single email address or list of email addresses to delete 95 | 96 | Returns: 97 | List[Tuple[str, bool, str]]: List of (email, success, message) tuples 98 | """ 99 | # Ensure email is a list 100 | emails = [email] if isinstance(email, str) else email 101 | 102 | if not emails: 103 | logging.error(getTranslation("no_emails_to_delete")) 104 | return [] 105 | 106 | # Get iCloud cookies from config 107 | try: 108 | # Get cookies from .env file 109 | cookies = os.getenv('ICLOUD_COOKIES', '').strip() 110 | if not cookies: 111 | logging.error(getTranslation("icloud_cookies_not_configured")) 112 | return [(e, False, getTranslation("icloud_cookies_not_configured")) for e in emails] 113 | 114 | # Process each email 115 | results = [] 116 | for e in emails: 117 | success, message = asyncio.run(_delete_email(cookies, e)) 118 | results.append((e, success, message)) 119 | if success: 120 | logging.info(message) 121 | else: 122 | logging.error(message) 123 | 124 | return results 125 | 126 | except Exception as e: 127 | logging.error(getTranslation("email_deletion_error").format(str(e))) 128 | import traceback 129 | logging.error(traceback.format_exc()) 130 | return [(e, False, str(e)) for e in emails] 131 | 132 | if __name__ == "__main__": 133 | # If run directly, delete specified emails 134 | if len(sys.argv) > 1: 135 | emails_to_delete = sys.argv[1:] 136 | print(getTranslation("deleting_emails").format(len(emails_to_delete))) 137 | results = deleteIcloudEmail(emails_to_delete) 138 | 139 | for email, success, message in results: 140 | status = getTranslation("success") if success else getTranslation("failed") 141 | print(f"{email}: {status} - {message}") 142 | else: 143 | print(getTranslation("no_emails_specified")) -------------------------------------------------------------------------------- /src/icloud/generateEmail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | iCloud Email Generator 4 | This module generates Hide My Email addresses for iCloud accounts. 5 | """ 6 | 7 | import os 8 | import sys 9 | import asyncio 10 | from typing import List, Optional 11 | 12 | # Add parent directory to path to import language module 13 | parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 14 | if parent_dir not in sys.path: 15 | sys.path.append(parent_dir) 16 | 17 | try: 18 | from src.utils.logger import logging 19 | from src.utils.config import Config 20 | from src.icloud.hidemyemail import HideMyEmail 21 | from src.utils.language import getTranslation, _ 22 | except ImportError: 23 | from utils.logger import logging 24 | from utils.config import Config 25 | from icloud.hidemyemail import HideMyEmail 26 | from utils.language import getTranslation, _ 27 | 28 | async def _generate_single_email(cookies: str, label: str = "Cursor-Auto-iCloud") -> Optional[str]: 29 | """ 30 | Generate a single iCloud Hide My Email address 31 | 32 | Args: 33 | cookies: iCloud cookies for authentication 34 | label: Label for the email 35 | 36 | Returns: 37 | str: The generated email address or None if failed 38 | """ 39 | try: 40 | async with HideMyEmail(label, cookies) as hme: 41 | # Generate email 42 | gen_result = await hme.generate_email() 43 | 44 | # Debug print the result 45 | logging.debug(f"API Response: {gen_result}") 46 | 47 | if not gen_result.get("success", False): 48 | logging.error(getTranslation("generate_email_failed").format(gen_result.get('reason', getTranslation("unknown_error")))) 49 | return None 50 | 51 | # Correctly access the email address from the nested structure 52 | email = gen_result.get("result", {}).get("hme") 53 | if not email: 54 | logging.error(getTranslation("generate_email_failed_no_address")) 55 | return None 56 | 57 | # Reserve email 58 | reserve_result = await hme.reserve_email(email) 59 | if not reserve_result.get("success", False): 60 | logging.error(getTranslation("reserve_email_failed").format(reserve_result.get('reason', getTranslation("unknown_error")))) 61 | return None 62 | 63 | logging.info(getTranslation("email_generated_success").format(email)) 64 | return email 65 | except Exception as e: 66 | logging.error(getTranslation("generate_email_error").format(str(e))) 67 | return None 68 | 69 | async def _generate_multiple_emails(count: int, cookies: str, label: str = "Cursor-Auto-iCloud") -> List[str]: 70 | """ 71 | Generate multiple iCloud Hide My Email addresses 72 | 73 | Args: 74 | count: Number of emails to generate 75 | cookies: iCloud cookies for authentication 76 | label: Label for the emails 77 | 78 | Returns: 79 | List[str]: List of generated email addresses 80 | """ 81 | tasks = [] 82 | for _ in range(count): 83 | tasks.append(_generate_single_email(cookies, label)) 84 | 85 | results = await asyncio.gather(*tasks) 86 | # Filter out None values 87 | return [email for email in results if email] 88 | 89 | def generateIcloudEmail(count: int = 1, save_to_file: bool = True) -> List[str]: 90 | """ 91 | Generate a specified number of iCloud Hide My Email addresses 92 | 93 | Args: 94 | count: Number of emails to generate 95 | save_to_file: Whether to save emails to data/emails.txt 96 | 97 | Returns: 98 | List[str]: List of generated email addresses 99 | """ 100 | # Get iCloud cookies from config 101 | try: 102 | # Get cookies from .env file 103 | cookies = os.getenv('ICLOUD_COOKIES', '').strip() 104 | if not cookies: 105 | logging.error(getTranslation("icloud_cookies_not_configured")) 106 | return [] 107 | 108 | # Generate emails 109 | logging.info(getTranslation("start_generating_emails").format(count)) 110 | emails = asyncio.run(_generate_multiple_emails(count, cookies)) 111 | 112 | if not emails: 113 | logging.error(getTranslation("no_emails_generated")) 114 | return [] 115 | 116 | logging.info(getTranslation("emails_generated_success").format(len(emails))) 117 | 118 | # Save to file if requested 119 | if save_to_file: 120 | data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "data") 121 | if not os.path.exists(data_dir): 122 | os.makedirs(data_dir) 123 | 124 | emails_file = os.path.join(data_dir, "emails.txt") 125 | 126 | # If file exists, read existing emails 127 | existing_emails = [] 128 | if os.path.exists(emails_file): 129 | with open(emails_file, "r") as f: 130 | existing_emails = [line.strip() for line in f.readlines() if line.strip()] 131 | 132 | # Add new emails 133 | all_emails = existing_emails + emails 134 | 135 | # Write back to file 136 | with open(emails_file, "w") as f: 137 | f.write("\n".join(all_emails)) 138 | 139 | logging.info(getTranslation("emails_saved_to_file").format(emails_file)) 140 | 141 | return emails 142 | 143 | except Exception as e: 144 | logging.error(getTranslation("generate_email_error").format(str(e))) 145 | import traceback 146 | logging.error(traceback.format_exc()) 147 | return [] 148 | 149 | if __name__ == "__main__": 150 | # If run directly, generate 5 emails 151 | count = 5 152 | if len(sys.argv) > 1: 153 | try: 154 | count = int(sys.argv[1]) 155 | except ValueError: 156 | logging.error(getTranslation("invalid_count_parameter").format(sys.argv[1])) 157 | sys.exit(1) 158 | 159 | emails = generateIcloudEmail(count) 160 | if emails: 161 | print(getTranslation("emails_generated_success").format(len(emails))) 162 | for email in emails: 163 | print(email) 164 | else: 165 | print(getTranslation("no_emails_generated")) 166 | -------------------------------------------------------------------------------- /src/icloud/hidemyemail.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import ssl 4 | import certifi 5 | import os 6 | import sys 7 | 8 | # Add parent directory to path to import language module 9 | parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 10 | if parent_dir not in sys.path: 11 | sys.path.append(parent_dir) 12 | 13 | try: 14 | from src.utils.logger import logging 15 | from src.utils.language import getTranslation, _ 16 | except ImportError: 17 | from utils.logger import logging 18 | from utils.language import getTranslation, _ 19 | 20 | 21 | class HideMyEmail: 22 | base_url_v1 = "https://p68-maildomainws.icloud.com/v1/hme" 23 | base_url_v2 = "https://p68-maildomainws.icloud.com/v2/hme" 24 | params = { 25 | "clientBuildNumber": "2413Project28", 26 | "clientMasteringNumber": "2413B20", 27 | "clientId": "", 28 | "dsid": "", # Directory Services Identifier (DSID) is a method of identifying AppleID accounts 29 | } 30 | 31 | def __init__(self, label: str = "Cursor-Auto-iCloud", cookies: str = ""): 32 | """Initializes the HideMyEmail class. 33 | 34 | Args: 35 | label (str) Label that will be set for all emails generated, defaults to `Cursor-Auto-iCloud` 36 | cookies (str) Cookie string to be used with requests. Required for authorization. 37 | """ 38 | self.label = label 39 | 40 | # Cookie string to be used with requests. Required for authorization. 41 | self.cookies = cookies 42 | 43 | async def __aenter__(self): 44 | connector = aiohttp.TCPConnector(ssl_context=ssl.create_default_context(cafile=certifi.where())) 45 | self.s = aiohttp.ClientSession( 46 | headers={ 47 | "Connection": "keep-alive", 48 | "Pragma": "no-cache", 49 | "Cache-Control": "no-cache", 50 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", 51 | "Content-Type": "text/plain", 52 | "Accept": "*/*", 53 | "Sec-GPC": "1", 54 | "Origin": "https://www.icloud.com", 55 | "Sec-Fetch-Site": "same-site", 56 | "Sec-Fetch-Mode": "cors", 57 | "Sec-Fetch-Dest": "empty", 58 | "Referer": "https://www.icloud.com/", 59 | "Accept-Language": "en-US,en-GB;q=0.9,en;q=0.8,cs;q=0.7", 60 | "Cookie": self.__cookies.strip(), 61 | }, 62 | timeout=aiohttp.ClientTimeout(total=10), 63 | connector=connector, 64 | ) 65 | 66 | return self 67 | 68 | async def __aexit__(self, exc_t, exc_v, exc_tb): 69 | await self.s.close() 70 | 71 | @property 72 | def cookies(self) -> str: 73 | return self.__cookies 74 | 75 | @cookies.setter 76 | def cookies(self, cookies: str): 77 | # remove new lines/whitespace for security reasons 78 | self.__cookies = cookies.strip() 79 | 80 | async def generate_email(self) -> dict: 81 | """Generates an email""" 82 | try: 83 | logging.debug(getTranslation("generating_icloud_hidden_email")) 84 | async with self.s.post( 85 | f"{self.base_url_v1}/generate", params=self.params, json={"langCode": "en-us"} 86 | ) as resp: 87 | res = await resp.json() 88 | return res 89 | except asyncio.TimeoutError: 90 | logging.error(getTranslation("generate_email_timeout")) 91 | return {"error": 1, "reason": getTranslation("request_timeout")} 92 | except Exception as e: 93 | logging.error(getTranslation("generate_email_failed").format(str(e))) 94 | return {"error": 1, "reason": str(e)} 95 | 96 | async def reserve_email(self, email: str) -> dict: 97 | """Reserves an email and registers it for forwarding""" 98 | try: 99 | logging.debug(getTranslation("reserving_email").format(email)) 100 | payload = { 101 | "hme": email, 102 | "label": self.label, 103 | "note": "Cursor-Auto-iCloud", 104 | } 105 | async with self.s.post( 106 | f"{self.base_url_v1}/reserve", params=self.params, json=payload 107 | ) as resp: 108 | res = await resp.json() 109 | return res 110 | except asyncio.TimeoutError: 111 | logging.error(getTranslation("reserve_email_timeout")) 112 | return {"error": 1, "reason": getTranslation("request_timeout")} 113 | except Exception as e: 114 | logging.error(getTranslation("reserve_email_failed").format(str(e))) 115 | return {"error": 1, "reason": str(e)} 116 | 117 | async def list_email(self) -> dict: 118 | """List all HME""" 119 | logging.info(getTranslation("getting_email_list")) 120 | try: 121 | async with self.s.get(f"{self.base_url_v2}/list", params=self.params) as resp: 122 | res = await resp.json() 123 | return res 124 | except asyncio.TimeoutError: 125 | logging.error(getTranslation("list_email_timeout")) 126 | return {"error": 1, "reason": getTranslation("request_timeout")} 127 | except Exception as e: 128 | logging.error(getTranslation("list_email_failed").format(str(e))) 129 | return {"error": 1, "reason": str(e)} 130 | 131 | async def deactivate_email(self, anonymous_id: str) -> dict: 132 | """Deactivates an email using its anonymousId""" 133 | logging.info(getTranslation("deactivating_email").format(anonymous_id)) 134 | try: 135 | async with self.s.post(f"{self.base_url_v1}/deactivate", params=self.params, json={"anonymousId": anonymous_id}) as resp: 136 | res = await resp.json() 137 | return res 138 | except asyncio.TimeoutError: 139 | logging.error(getTranslation("deactivate_email_timeout")) 140 | return {"error": 1, "reason": getTranslation("request_timeout")} 141 | except Exception as e: 142 | logging.error(getTranslation("deactivate_email_failed").format(str(e))) 143 | return {"error": 1, "reason": str(e)} 144 | 145 | async def delete_email(self, anonymous_id: str, email: str = None) -> dict: 146 | """Deletes an email using its anonymousId 147 | 148 | Args: 149 | anonymous_id: The anonymousId of the email to delete 150 | email: Optional email address (for logging purposes only) 151 | """ 152 | log_identifier = email if email else anonymous_id 153 | logging.info(getTranslation("deleting_email").format(log_identifier)) 154 | try: 155 | payload = {"anonymousId": anonymous_id} 156 | async with self.s.post(f"{self.base_url_v1}/delete", params=self.params, json=payload) as resp: 157 | res = await resp.json() 158 | print(res) 159 | return res 160 | except asyncio.TimeoutError: 161 | logging.error(getTranslation("delete_email_timeout")) 162 | return {"error": 1, "reason": getTranslation("request_timeout")} 163 | except Exception as e: 164 | print(e) 165 | logging.error(getTranslation("delete_email_failed").format(str(e))) 166 | return {"error": 1, "reason": str(e)} 167 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Cursor Pro Keep Alive - Main Entry Point 4 | This script serves as the main entry point for the Cursor Pro Keep Alive application. 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | # Add the parent directory to the path to import the local packages 11 | parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 12 | if parent_dir not in sys.path: 13 | sys.path.append(parent_dir) 14 | 15 | # Handle frozen environment (PyInstaller) 16 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): 17 | # Running in a PyInstaller bundle 18 | bundle_dir = sys._MEIPASS 19 | # Add the bundle directory to path 20 | if bundle_dir not in sys.path: 21 | sys.path.append(bundle_dir) 22 | # Add the src directory in the bundle 23 | src_dir = os.path.join(bundle_dir, 'src') 24 | if os.path.exists(src_dir) and src_dir not in sys.path: 25 | sys.path.append(src_dir) 26 | 27 | # Import logger first to ensure it's initialized before other modules 28 | try: 29 | from src.utils.logger import logging 30 | from src.core.cursor_pro_keep_alive import main as cursor_keep_alive_main 31 | from src.ui.logo import print_logo 32 | from src.utils.language import getTranslation 33 | except ImportError: 34 | # If direct import fails, try relative import 35 | from utils.logger import logging 36 | from core.cursor_pro_keep_alive import main as cursor_keep_alive_main 37 | from ui.logo import print_logo 38 | from utils.language import getTranslation 39 | 40 | def main(): 41 | """Main entry point for the application.""" 42 | print_logo() 43 | logging.info(getTranslation("application_starting")) 44 | try: 45 | cursor_keep_alive_main() 46 | except Exception as e: 47 | logging.error(getTranslation("application_error").format(str(e))) 48 | import traceback 49 | logging.error(traceback.format_exc()) 50 | return 1 51 | return 0 52 | 53 | if __name__ == "__main__": 54 | sys.exit(main()) -------------------------------------------------------------------------------- /src/turnstilePatch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Turnstile Patcher", 4 | "version": "2.1", 5 | "content_scripts": [ 6 | { 7 | "js": [ 8 | "./script.js" 9 | ], 10 | "matches": [ 11 | "" 12 | ], 13 | "run_at": "document_start", 14 | "all_frames": true, 15 | "world": "MAIN" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /src/turnstilePatch/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/turnstilePatch/script.js: -------------------------------------------------------------------------------- 1 | function getRandomInt(min, max) { 2 | return Math.floor(Math.random() * (max - min + 1)) + min; 3 | } 4 | 5 | // old method wouldn't work on 4k screens 6 | 7 | let screenX = getRandomInt(800, 1200); 8 | let screenY = getRandomInt(400, 600); 9 | 10 | Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX }); 11 | 12 | Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY }); -------------------------------------------------------------------------------- /src/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryan0204/cursor-auto-icloud/ea87ee415143cdd990dcb9069b315d6b887d432f/src/ui/__init__.py -------------------------------------------------------------------------------- /src/ui/logo.py: -------------------------------------------------------------------------------- 1 | CURSOR_LOGO = """ 2 | ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ 3 | ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ 4 | ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ 5 | ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ 6 | ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ 7 | ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ 8 | """ 9 | 10 | 11 | def print_logo(): 12 | print(CURSOR_LOGO) 13 | 14 | 15 | if __name__ == "__main__": 16 | print_logo() 17 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Import common utilities for easier access 2 | from src.utils.language import LanguageManager, Language, _, getTranslation 3 | -------------------------------------------------------------------------------- /src/utils/browser_utils.py: -------------------------------------------------------------------------------- 1 | from DrissionPage import ChromiumOptions, Chromium 2 | import sys 3 | import os 4 | import logging 5 | from dotenv import load_dotenv 6 | 7 | load_dotenv() 8 | 9 | 10 | class BrowserManager: 11 | def __init__(self): 12 | self.browser = None 13 | 14 | def init_browser(self, user_agent=None): 15 | """初始化浏览器""" 16 | co = self._get_browser_options(user_agent) 17 | self.browser = Chromium(co) 18 | return self.browser 19 | 20 | def _get_browser_options(self, user_agent=None): 21 | """获取浏览器配置""" 22 | co = ChromiumOptions() 23 | try: 24 | extension_path = self._get_extension_path("turnstilePatch") 25 | co.add_extension(extension_path) 26 | except FileNotFoundError as e: 27 | logging.warning(f"警告: {e}") 28 | 29 | browser_path = os.getenv("BROWSER_PATH") 30 | if browser_path: 31 | co.set_paths(browser_path=browser_path) 32 | 33 | co.set_pref("credentials_enable_service", False) 34 | co.set_argument("--hide-crash-restore-bubble") 35 | proxy = os.getenv("BROWSER_PROXY") 36 | if proxy: 37 | co.set_proxy(proxy) 38 | 39 | co.auto_port() 40 | if user_agent: 41 | co.set_user_agent(user_agent) 42 | 43 | co.headless( 44 | os.getenv("BROWSER_HEADLESS", "True").lower() == "true" 45 | ) # 生产环境使用无头模式 46 | 47 | # Mac 系统特殊处理 48 | if sys.platform == "darwin": 49 | co.set_argument("--no-sandbox") 50 | co.set_argument("--disable-gpu") 51 | 52 | return co 53 | 54 | def _get_extension_path(self, exname='turnstilePatch'): 55 | """获取插件路径""" 56 | # First try to locate the extension in the src folder 57 | root_dir = os.getcwd() 58 | extension_path = os.path.join(root_dir, "src", exname) 59 | 60 | # If not found in src, try the root directory 61 | if not os.path.exists(extension_path): 62 | extension_path = os.path.join(root_dir, exname) 63 | 64 | # For PyInstaller bundles 65 | if hasattr(sys, "_MEIPASS"): 66 | extension_path = os.path.join(sys._MEIPASS, "src", exname) 67 | if not os.path.exists(extension_path): 68 | extension_path = os.path.join(sys._MEIPASS, exname) 69 | 70 | if not os.path.exists(extension_path): 71 | raise FileNotFoundError(f"插件不存在: {extension_path}") 72 | 73 | return extension_path 74 | 75 | def quit(self): 76 | """关闭浏览器""" 77 | if self.browser: 78 | try: 79 | self.browser.quit() 80 | except: 81 | pass 82 | -------------------------------------------------------------------------------- /src/utils/config.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | import os 3 | import sys 4 | import json 5 | 6 | # Add parent directory to path to import language module 7 | parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 8 | if parent_dir not in sys.path: 9 | sys.path.append(parent_dir) 10 | 11 | try: 12 | from src.utils.logger import logging 13 | from src.utils.language import getTranslation, _ 14 | except ImportError: 15 | from utils.logger import logging 16 | try: 17 | from utils.language import getTranslation, _ 18 | except ImportError: 19 | # Fallback if language module is not available 20 | def getTranslation(key, *args): 21 | if args: 22 | return key.format(*args) 23 | return key 24 | 25 | 26 | class Config: 27 | def __init__(self): 28 | # 获取应用程序的根目录路径 29 | if getattr(sys, "frozen", False): 30 | # 如果是打包后的可执行文件 31 | application_path = os.path.dirname(sys.executable) 32 | else: 33 | # 如果是开发环境 34 | # Look for .env in the project root, not in src/utils 35 | application_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 36 | 37 | # 指定 .env 文件的路径 38 | dotenv_path = os.path.join(application_path, ".env") 39 | 40 | if not os.path.exists(dotenv_path): 41 | raise FileNotFoundError(getTranslation("env_file_not_exist").format(dotenv_path)) 42 | 43 | # 加载 .env 文件 44 | load_dotenv(dotenv_path) 45 | 46 | self.icloud_user = os.getenv('ICLOUD_USER', '').strip() 47 | if '@icloud.com' in self.icloud_user: 48 | self.icloud_user = self.icloud_user.replace('@icloud.com', '') 49 | self.icloud_pass = os.getenv('ICLOUD_APP_PASSWORD', '').strip() 50 | 51 | # 检查配置 52 | self.check_config() 53 | 54 | 55 | def get_icloud_imap(self): 56 | """获取 iCloud IMAP 配置 57 | 58 | Returns: 59 | dict or False: iCloud IMAP 配置信息,若未配置则返回 False 60 | """ 61 | # 检查必要的 iCloud IMAP 配置是否存在 62 | icloud_user = os.getenv('ICLOUD_USER', '').strip() 63 | 64 | if '@icloud.com' in icloud_user: 65 | icloud_user = icloud_user.replace('@icloud.com', '') 66 | 67 | icloud_pass = os.getenv('ICLOUD_APP_PASSWORD', '').strip() 68 | 69 | if not icloud_user or not icloud_pass: 70 | return False 71 | 72 | return { 73 | "imap_server": "imap.mail.me.com", # iCloud Mail 固定服务器 74 | "imap_port": 993, # iCloud Mail 固定端口 75 | "imap_user": icloud_user, # 用户名通常是邮箱前缀 76 | "imap_pass": icloud_pass, # 应用专用密码 77 | "imap_dir": os.getenv('ICLOUD_FOLDER', 'INBOX').strip(), 78 | } 79 | 80 | def check_config(self): 81 | """检查配置项是否有效 82 | 83 | 检查规则: 84 | 1. Validate if icloud user and pass is not null 85 | """ 86 | 87 | required_configs = { 88 | "icloud_user": getTranslation("icloud_email"), 89 | "icloud_pass": getTranslation("icloud_app_password"), 90 | } 91 | 92 | # 检查基础配置 93 | for key, name in required_configs.items(): 94 | if not self.check_is_valid(getattr(self, key)): 95 | raise ValueError(getTranslation("config_not_set").format(name=name, key=key.upper())) 96 | 97 | 98 | 99 | def check_is_valid(self, value): 100 | """检查配置项是否有效 101 | 102 | Args: 103 | value: 配置项的值 104 | 105 | Returns: 106 | bool: 配置项是否有效 107 | """ 108 | return isinstance(value, str) and len(str(value).strip()) > 0 109 | 110 | def print_config(self): 111 | logging.info(getTranslation("icloud_email_info").format(self.icloud_user)) 112 | logging.info(getTranslation("icloud_password_info").format(self.icloud_pass)) 113 | 114 | 115 | # 使用示例 116 | if __name__ == "__main__": 117 | try: 118 | config = Config() 119 | print(getTranslation("env_loaded_success")) 120 | config.print_config() 121 | except ValueError as e: 122 | print(getTranslation("error_message").format(str(e))) 123 | -------------------------------------------------------------------------------- /src/utils/get_email_code.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import logging 3 | import time 4 | import re 5 | from config import Config 6 | import requests 7 | import email 8 | import imaplib 9 | import poplib 10 | from email.parser import Parser 11 | import json 12 | import socket 13 | 14 | # Import translation functions 15 | from src.utils.language import getTranslation, _ 16 | 17 | 18 | class EmailVerificationHandler: 19 | def __init__(self,account): 20 | self.session = requests.Session() 21 | self.account = account 22 | 23 | def get_verification_code(self, max_retries=5, retry_interval=60): 24 | """ 25 | 获取验证码,带有重试机制。 26 | 27 | Args: 28 | max_retries: 最大重试次数。 29 | retry_interval: 重试间隔时间(秒)。 30 | 31 | Returns: 32 | 验证码 (字符串或 None)。 33 | """ 34 | 35 | for attempt in range(max_retries): 36 | try: 37 | logging.info(getTranslation("verification_code_attempt").format(attempt + 1, max_retries)) 38 | 39 | verify_code = self._get_latest_mail_code() 40 | if attempt < max_retries - 1 and not verify_code: # 除了最后一次尝试,都等待 41 | logging.warning(getTranslation("verification_code_not_found_retry").format(retry_interval)) 42 | time.sleep(retry_interval) 43 | else: 44 | return verify_code 45 | 46 | except Exception as e: 47 | logging.error(getTranslation("verification_code_fetch_failed").format(e)) # 记录更一般的异常 48 | if attempt < max_retries - 1: 49 | logging.error(getTranslation("error_will_retry").format(retry_interval)) 50 | time.sleep(retry_interval) 51 | else: 52 | raise Exception(getTranslation("max_retries_reached_with_error").format(e)) from e 53 | 54 | raise Exception(getTranslation("verification_code_not_found_after_attempts").format(max_retries)) 55 | 56 | # 手动输入验证码 57 | def _get_latest_mail_code(self): 58 | """ 59 | 获取最新邮件中的验证码: 60 | 1. iCloud IMAP 61 | 62 | Returns: 63 | str or tuple: 验证码或 (验证码, 邮件ID) 元组 64 | """ 65 | # 首先尝试 iCloud IMAP 66 | icloud_imap = Config().get_icloud_imap() 67 | if icloud_imap: 68 | logging.info(getTranslation("using_icloud_imap")) 69 | # First check inbox 70 | verify_code = self._get_mail_code_by_icloud_imap(icloud_imap) 71 | if verify_code: 72 | return verify_code 73 | 74 | # If no code found in inbox, check spam/junk folders 75 | logging.info(getTranslation("checking_spam_folders")) 76 | verify_code = self._check_spam_folders(icloud_imap) 77 | if verify_code: 78 | return verify_code 79 | 80 | return None 81 | 82 | def _check_spam_folders(self, icloud_config): 83 | """Check spam and junk folders for verification code 84 | 85 | Args: 86 | icloud_config: iCloud IMAP configuration 87 | 88 | Returns: 89 | str or None: verification code if found 90 | """ 91 | # Common spam/junk folder names in email services 92 | spam_folders = ['Junk', 'Spam', 'Bulk Mail', 'Junk E-mail'] 93 | 94 | # Get list of available folders first 95 | try: 96 | mail = imaplib.IMAP4_SSL(icloud_config['imap_server'], icloud_config['imap_port']) 97 | mail.login(icloud_config['imap_user'], icloud_config['imap_pass']) 98 | 99 | status, folder_list = mail.list() 100 | mail.logout() 101 | 102 | if status != 'OK': 103 | logging.error(getTranslation("icloud_folder_list_failed")) 104 | return None 105 | 106 | # Parse folder names from the response 107 | available_folders = [] 108 | for folder_info in folder_list: 109 | if isinstance(folder_info, bytes): 110 | parts = folder_info.decode('utf-8', errors='ignore').split('"') 111 | if len(parts) > 1: 112 | folder_name = parts[-2] 113 | available_folders.append(folder_name) 114 | 115 | # Filter spam folders that actually exist 116 | valid_spam_folders = [folder for folder in spam_folders if folder in available_folders] 117 | 118 | # If no valid spam folders found, try the default list 119 | if not valid_spam_folders: 120 | valid_spam_folders = spam_folders 121 | 122 | logging.info(f"Available spam folders: {valid_spam_folders}") 123 | 124 | except Exception as e: 125 | logging.error(getTranslation("icloud_folder_list_failed") + f": {str(e)}") 126 | # Continue with the default list if we can't get available folders 127 | valid_spam_folders = spam_folders 128 | 129 | # Check each potential spam folder 130 | for folder in valid_spam_folders: 131 | try: 132 | # Create a new connection for each folder to avoid state issues 133 | mail = None 134 | try: 135 | mail = imaplib.IMAP4_SSL(icloud_config['imap_server'], icloud_config['imap_port']) 136 | mail.login(icloud_config['imap_user'], icloud_config['imap_pass']) 137 | mail.select(folder) 138 | except Exception as e: 139 | logging.error(getTranslation("error_checking_folder").format(folder, f"Failed to select folder: {e}")) 140 | if mail: 141 | try: 142 | mail.logout() 143 | except: 144 | pass 145 | continue # Try next folder 146 | 147 | logging.info(getTranslation("checking_folder").format(folder)) 148 | 149 | try: 150 | status, messages = mail.search(None, 'ALL') 151 | if status != 'OK': 152 | logging.error(f"Search failed in folder {folder}: {status}") 153 | mail.logout() 154 | continue # Try next folder 155 | 156 | # Handle different message types (bytes or string) 157 | if isinstance(messages[0], bytes): 158 | mail_ids = messages[0].split() 159 | elif isinstance(messages[0], str): 160 | mail_ids = messages[0].encode('utf-8').split() 161 | else: 162 | mail_ids = [] 163 | 164 | if not mail_ids: 165 | logging.info(f"No emails in folder {folder}") 166 | mail.logout() 167 | continue # No emails in this folder 168 | 169 | # Check the latest 10 emails in this folder 170 | for i in range(min(10, len(mail_ids))): 171 | mail_id = mail_ids[len(mail_ids) - 1 - i] 172 | try: 173 | status, msg_data = mail.fetch(mail_id, '(BODY[])') 174 | except (EOFError, ConnectionError, socket.error) as e: 175 | logging.error(getTranslation("icloud_imap_fetch_failed").format(e)) 176 | break # Connection issue, try next folder 177 | 178 | if status != 'OK' or not msg_data or not msg_data[0]: 179 | logging.error(getTranslation("icloud_imap_fetch_status_failed").format(status)) 180 | continue 181 | 182 | # Safety check for data structure 183 | if not isinstance(msg_data[0], tuple) or len(msg_data[0]) < 2: 184 | logging.error(f"Unexpected message data structure: {msg_data}") 185 | continue 186 | 187 | raw_email = msg_data[0][1] 188 | if not raw_email: 189 | continue 190 | 191 | email_message = email.message_from_bytes(raw_email) 192 | sender = email_message.get('from', '') 193 | recipient = email_message.get('to', '') 194 | 195 | if self.account not in recipient: 196 | continue 197 | if 'no-reply_at_cursor_sh' not in sender: 198 | continue 199 | 200 | body = self._extract_imap_body(email_message) 201 | if body: 202 | # Look for 6-digit verification code 203 | code_match = re.search(r"(? 0: 243 | time.sleep(3) 244 | if retry >= 20: 245 | raise Exception(getTranslation("verification_code_timeout")) 246 | 247 | try: 248 | # 连接到 iCloud IMAP 服务器 249 | mail = imaplib.IMAP4_SSL(icloud_config['imap_server'], icloud_config['imap_port']) 250 | 251 | mail.login(icloud_config['imap_user'], icloud_config['imap_pass']) 252 | mail.select(icloud_config['imap_dir'] or 'INBOX') 253 | 254 | # 获取最近的邮件 255 | status, messages = mail.search(None, 'ALL') 256 | if status != 'OK': 257 | logging.error(getTranslation("icloud_email_list_failed").format(status)) 258 | return None 259 | 260 | mail_ids = messages[0].split() 261 | print(mail_ids) 262 | if not mail_ids: 263 | # 没有获取到邮件 264 | logging.info(getTranslation("no_emails_in_icloud")) 265 | return self._get_mail_code_by_icloud_imap(icloud_config, retry=retry + 1) 266 | 267 | # 检查最新的10封邮件 268 | for i in range(min(10, len(mail_ids))): 269 | mail_id = mail_ids[len(mail_ids) - 1 - i] 270 | try: 271 | status, msg_data = mail.fetch(mail_id, '(BODY[])') 272 | except (EOFError, ConnectionError, socket.error) as e: 273 | logging.error(getTranslation("icloud_imap_fetch_failed").format(e)) 274 | mail.logout() 275 | return None 276 | if status != 'OK': 277 | logging.error(getTranslation("icloud_imap_fetch_status_failed").format(status)) 278 | continue 279 | raw_email = msg_data[0][1] 280 | 281 | email_message = email.message_from_bytes(raw_email) 282 | sender = email_message.get('from', '') 283 | recipient = email_message.get('to', '') 284 | 285 | if self.account not in recipient: 286 | continue 287 | if 'no-reply_at_cursor_sh' not in sender: 288 | continue 289 | 290 | 291 | body = self._extract_imap_body(email_message) 292 | if body: 293 | # 查找 6 位数验证码 294 | code_match = re.search(r"(?