├── .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 | [](https://github.com/ryan0204/cursor-auto-icloud/releases/latest)
6 | [](https://github.com/ryan0204/cursor-auto-icloud/stargazers)
7 |
8 |
9 | ## ⭐️ 在 GitHub 上给我们 Star — 这对我们是很大的鼓励!
10 |
11 | [🌏 English README](README.md)
12 |
13 |

14 |
15 |

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 | [](https://github.com/ryan0204/cursor-auto-icloud/releases/latest)
6 | [](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 |

14 |
15 |

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"(?