msteams-transcripts 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.5
2
+ Name: msteams-transcripts
3
+ Version: 1.0.0
4
+ Summary: List and download Microsoft Teams meeting transcripts from the command line, through your own signed-in browser session.
5
+ Project-URL: Homepage, https://github.com/divyavanmahajan/msteams-transcripts
6
+ Project-URL: Repository, https://github.com/divyavanmahajan/msteams-transcripts
7
+ Project-URL: Issues, https://github.com/divyavanmahajan/msteams-transcripts/issues
8
+ Author-email: Divya Van Mahajan <divyavanmahajan@users.noreply.github.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Divya Van Mahajan
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: cli,meeting,microsoft-teams,recap,sharepoint,transcript
32
+ Classifier: Development Status :: 5 - Production/Stable
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: End Users/Desktop
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: MacOS
37
+ Classifier: Operating System :: Microsoft :: Windows
38
+ Classifier: Operating System :: POSIX :: Linux
39
+ Classifier: Programming Language :: Python :: 3
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Programming Language :: Python :: 3.13
44
+ Classifier: Topic :: Communications :: Conferencing
45
+ Classifier: Topic :: Utilities
46
+ Requires-Python: >=3.10
47
+ Requires-Dist: playwright>=1.40
48
+ Provides-Extra: dev
49
+ Requires-Dist: build>=1.0; extra == 'dev'
50
+ Requires-Dist: hatch>=1.9; extra == 'dev'
51
+ Requires-Dist: pytest>=7; extra == 'dev'
52
+ Requires-Dist: twine>=5.0; extra == 'dev'
53
+ Description-Content-Type: text/markdown
54
+
55
+ # msteams-transcripts
56
+
57
+ Download Microsoft Teams meeting transcripts from the command line, including
58
+ the meetings where the Download button is greyed out.
59
+
60
+ It drives a browser you are already signed in to, so it needs no admin consent,
61
+ no app registration and no Graph permissions, and it reaches exactly the
62
+ meetings you can open yourself. Anything you can read in the Recap tab, this
63
+ can save to a file.
64
+
65
+ ```powershell
66
+ uvx msteams-transcripts browser
67
+ uvx msteams-transcripts list --from 2026-09-01 --to today
68
+ uvx msteams-transcripts get 3 --details
69
+ ```
70
+
71
+ ## Why the Download button being disabled does not matter
72
+
73
+ - **Listing meetings.** The Teams web client loads your calendar from its
74
+ middle-tier API. Each online meeting carries a thread identifier.
75
+ - **Finding the transcript.** The Recap tab asks the meeting-content service
76
+ about that thread. The reply lists resources; a transcript resource holds the
77
+ OneDrive drive, item and transcript identifiers.
78
+ - **Reading the transcript.** The recap page renders it from SharePoint, at
79
+ `/_api/v2.1/drives/<driveId>/items/<itemId>/media/transcripts/<id>/content`,
80
+ in JSON or WebVTT. That call is authenticated by your SharePoint cookies
81
+ alone. The whole transcript is already delivered to your browser in one
82
+ request, so no scrolling or scraping is involved. The disabled button is a
83
+ user-interface policy, not an access control.
84
+ - **Tokens.** The two Teams APIs want bearer tokens that the web client keeps
85
+ encrypted in local storage. The tool captures them from the requests the
86
+ Teams tab makes as it loads.
87
+
88
+ ## Install
89
+
90
+ With [uv](https://docs.astral.sh/uv/), nothing to install:
91
+
92
+ ```powershell
93
+ uvx msteams-transcripts --help
94
+ ```
95
+
96
+ Or install it permanently:
97
+
98
+ ```powershell
99
+ uv tool install msteams-transcripts # or: pipx install msteams-transcripts
100
+ pip install msteams-transcripts # into the current environment
101
+ ```
102
+
103
+ Both `msteams-transcripts` and the shorter `teams-transcripts` are installed as
104
+ commands. Python 3.10 or newer is required. Playwright comes as a dependency,
105
+ but **do not run `playwright install`**: the tool attaches to the Edge or Chrome
106
+ already on your machine and downloads no browsers of its own.
107
+
108
+ ## Walkthrough
109
+
110
+ A first session from a clean machine, start to finish.
111
+
112
+ **1. Start the browser.**
113
+
114
+ ```powershell
115
+ uvx msteams-transcripts browser
116
+ ```
117
+
118
+ An Edge window opens on Teams using a profile of its own. On a corporate,
119
+ Entra-joined PC it usually signs in by itself. If a sign-in page appears,
120
+ complete it there once. Leave the window open; you can minimise it. Recent Edge
121
+ and Chrome refuse remote debugging on your everyday profile, which is why a
122
+ separate one is used.
123
+
124
+ **2. See what is available.**
125
+
126
+ ```powershell
127
+ uvx msteams-transcripts list --from 2026-09-01 --to today
128
+ ```
129
+
130
+ ```
131
+ # Start (UTC) Subject Organizer
132
+ 1 2026-09-15 05:41 Weekly one to one Priya Raman
133
+ 2 2026-09-15 06:09 Platform team sync Tomas Lindqvist
134
+ 3 2026-09-16 09:02 Architecture review: storage tiering Maya Okonkwo
135
+ 12 transcript(s). Use: msteams-transcripts get <#>
136
+ ```
137
+
138
+ The first run takes 30 to 60 seconds while Teams loads and issues its tokens;
139
+ later runs in the same browser session are quicker. Rows are transcripts rather
140
+ than meetings, so a recurring meeting recorded twice appears twice.
141
+
142
+ **3. Download one transcript with the meeting details.**
143
+
144
+ ```powershell
145
+ uvx msteams-transcripts get 3 --details
146
+ ```
147
+
148
+ This writes `transcripts\2026-09-16 0902 - Architecture review storage tiering.txt`:
149
+
150
+ ```
151
+ # Architecture review: storage tiering
152
+ # Date: 2026-09-16 09:02
153
+ # Source: https://contoso-my.sharepoint.com/personal/.../Recordings/Architecture-review....mp4
154
+ # Organizer: Maya Okonkwo <maya.okonkwo@example.com>
155
+ # Location: Microsoft Teams Meeting
156
+ # Invited (13):
157
+ # Priya Raman <priya.raman@example.com> (Required, Accepted)
158
+ # Tomas Lindqvist <tomas.lindqvist@example.com> (Required, Tentative)
159
+ # ...
160
+ # Speakers in transcript (5):
161
+ # Priya Raman (84 segments)
162
+ # Maya Okonkwo (72 segments)
163
+ # ...
164
+ # Files shared in meeting chat (1):
165
+ # capacity-plan.xlsx https://contoso.sharepoint.com/sites/...
166
+ # Invitation text:
167
+ # Agenda: ...
168
+
169
+ [00:00:03] Priya Raman:
170
+ Good morning everyone, let's start with the architecture review.
171
+ ```
172
+
173
+ A sidecar `...meta.json` holds the same details as structured data: attendees
174
+ with response status, speakers with segment counts, shared files, invitation
175
+ text and the thread identifier.
176
+
177
+ **4. Download from a link instead.** Open the meeting in Teams, go to the Recap
178
+ tab, copy the address bar, and pass it:
179
+
180
+ ```powershell
181
+ uvx msteams-transcripts get "https://teams.cloud.microsoft/l/meetingrecap?driveId=...&driveItemId=...&sitePath=..." --details -f all
182
+ ```
183
+
184
+ This path does not touch the calendar and finishes in a few seconds.
185
+
186
+ **5. Archive a whole period.**
187
+
188
+ ```powershell
189
+ uvx msteams-transcripts batch --from 2026-09-01 --to 2026-09-30 --details -o C:\Archive\teams
190
+ uvx msteams-transcripts batch --from 2026-09-01 --to 2026-09-30 --only 3,7,12
191
+ ```
192
+
193
+ Files with the same name are overwritten. Batch ends with a count of what was
194
+ downloaded and what failed.
195
+
196
+ **6. Skipping the "Stay better connected with the Teams desktop app" popup**
197
+ when you open recap links yourself: change `/l/meetingrecap?` in the address to
198
+ `/_#/l/meetingrecap?`. That form opens the web client directly. The tool
199
+ already uses it internally.
200
+
201
+ ## Commands
202
+
203
+ ```
204
+ msteams-transcripts browser
205
+ msteams-transcripts list --from YYYY-MM-DD --to YYYY-MM-DD|today [--json]
206
+ msteams-transcripts get <row#|recap-url|thread-id> [-d] [-f txt|json|vtt|all] [-o DIR]
207
+ msteams-transcripts batch --from D --to D [-d] [-f FMT] [-o DIR] [--only 3,7,12]
208
+ ```
209
+
210
+ | Option | Meaning |
211
+ |---|---|
212
+ | `-d`, `--details` | organizer, location, invited attendees with response status, speakers, shared files, invitation text, plus a `.meta.json` sidecar |
213
+ | `-f`, `--format` | `txt` (default), `json` for the raw Stream document, `vtt` for WebVTT, or `all` |
214
+ | `-o`, `--out` | output directory, `./transcripts` by default |
215
+ | `--json` | on `list`, print the rows as JSON for scripting |
216
+ | `--only` | on `batch`, restrict to given row numbers |
217
+ | `--port`, `--profile`, `--browser` | override the debugging port, profile directory or browser executable |
218
+
219
+ The text format is:
220
+
221
+ ```
222
+ [00:00:03] Speaker Name:
223
+ what they said
224
+ the next sentence by the same speaker
225
+ ```
226
+
227
+ Timestamps are offsets from the start of the recording, not wall-clock time.
228
+ Row numbers used by `get N` come from the most recent `list`, cached in
229
+ `~/.teams_transcripts/last_list.json`.
230
+
231
+ ## Using it from an agent
232
+
233
+ `.claude/skills/teams-transcripts/SKILL.md` describes the tool for Claude Code
234
+ or another agent: preconditions, commands, output format, typical flows and
235
+ error handling. Claude Code picks it up automatically when working in this
236
+ project.
237
+
238
+ ## Library use
239
+
240
+ ```python
241
+ import asyncio
242
+ from pathlib import Path
243
+ from teams_transcripts import TeamsSession, download_ref, refs_from_recap_url
244
+
245
+ async def main():
246
+ async with TeamsSession() as s:
247
+ ref = refs_from_recap_url("https://teams.cloud.microsoft/l/meetingrecap?...")
248
+ await download_ref(s, ref, Path("out"), "txt", details=True)
249
+
250
+ asyncio.run(main())
251
+ ```
252
+
253
+ Anything the user can act on raises `TranscriptError`.
254
+
255
+ ## Environment variables
256
+
257
+ | Variable | Default | Purpose |
258
+ |---|---|---|
259
+ | `TT_CDP_PORT` | `9222` | browser debugging port |
260
+ | `TT_PROFILE_DIR` | platform application-data directory | browser profile |
261
+ | `TT_BROWSER_EXE` | Edge, then Chrome, from the usual locations | browser executable |
262
+ | `TT_STATE_DIR` | `~/.teams_transcripts` | where the last listing is cached |
263
+
264
+ ## Troubleshooting
265
+
266
+ - **"The browser did not expose its debugging port"**: another window is
267
+ already using the dedicated profile without the port. Close it, then run
268
+ `browser` again.
269
+ - **"Could not capture a Teams token"**: the Teams tab in that window is not
270
+ signed in. Sign in there, then retry.
271
+ - **"Could not capture the meeting-content token"**: open any meeting's Recap
272
+ tab in that window, then retry.
273
+ - **HTTP 403 when listing or downloading**: the recording belongs to a tenant
274
+ or site you cannot read. The Recap tab would be empty for you too.
275
+ - **Not available through this tool**: the attendance report of who actually
276
+ joined and when, collaborative Loop notes, and Copilot AI notes. `--details`
277
+ gives the invited list from the calendar and who actually spoke per the
278
+ transcript.
279
+ - **Recurring meetings** share one thread identifier. Each occurrence's
280
+ transcript is listed separately with its own start time.
281
+
282
+ ## Development
283
+
284
+ ```powershell
285
+ git clone https://github.com/divyavanmahajan/msteams-transcripts
286
+ cd msteams-transcripts
287
+ pip install -e ".[dev]"
288
+ python -m pytest
289
+ ```
290
+
291
+ The tests cover the pure functions in `model.py` and `formats.py` and need
292
+ neither a browser nor the network.
293
+
294
+ Hatchling owns the version, read from `__version__` in
295
+ `src/teams_transcripts/__init__.py`. To release:
296
+
297
+ ```powershell
298
+ hatch version patch # or minor, major, or an explicit 1.2.3
299
+ git commit -am "Release 0.1.1"
300
+ git tag v0.1.1
301
+ git push --follow-tags
302
+ ```
303
+
304
+ Pushing the tag runs `.github/workflows/publish.yml`, which refuses to continue
305
+ if the tag and the package version disagree, then builds, publishes to PyPI
306
+ through trusted publishing and creates a GitHub release. Trusted publishing is
307
+ configured once on PyPI under Publishing, naming this repository, the
308
+ `publish.yml` workflow and the `pypi` environment; no API token is stored.
309
+ Running the workflow by hand offers TestPyPI as the target.
310
+
311
+ ## Licence
312
+
313
+ MIT. Transcripts are meeting content belonging to the people in them; this tool
314
+ only fetches what you already have access to, and what you do with it afterwards
315
+ is your responsibility.
@@ -0,0 +1,15 @@
1
+ teams_transcripts/__init__.py,sha256=9w-YZn2HIpmtNwEXfmGzmYT4S-u3R4z7yPErrEnIbKc,1611
2
+ teams_transcripts/__main__.py,sha256=1Bj6t22eNhGRqEMkIGpEG6N5GvoAog7gkkWhBJQA0Cc,140
3
+ teams_transcripts/browser.py,sha256=fDrtNxNBxWeCCjmOE8QEj9vdss5zGb8dHe59Igr1iko,2472
4
+ teams_transcripts/cli.py,sha256=HPMQiAHwLiLNAHPyaECAbenfdUM-vme1prBUSOZIzz0,10009
5
+ teams_transcripts/config.py,sha256=QNvP88gJxaQt7j6I9AM_gj9Nemp6zIJmeyfWkny2xuI,3053
6
+ teams_transcripts/download.py,sha256=PWMU4OotAiM8477MGJutv4x4XvlDNpPtJg5w0XwtDs4,4662
7
+ teams_transcripts/errors.py,sha256=3zR5u_bJvbaKeXgswcKT_usEnLXHQwkvqGFUNSQuVD0,319
8
+ teams_transcripts/formats.py,sha256=Pg8ghbdtACdo9BgnEq7MCX_cF2UfrDLQaft-4NdbaVo,4639
9
+ teams_transcripts/model.py,sha256=_TrDSGZDL2EnkXUSvRPViEVljdKWBdBkBo-KCt-esvY,6406
10
+ teams_transcripts/session.py,sha256=nR3QNyTWYhDcs7tG2xlJjm8CchDYBZcNH5dlavfBzo4,11707
11
+ msteams_transcripts-1.0.0.dist-info/METADATA,sha256=AXWuvDrgUwbypzDkhr2YMqAzGnXhroIQSuZOp3VzHX4,12651
12
+ msteams_transcripts-1.0.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
13
+ msteams_transcripts-1.0.0.dist-info/entry_points.txt,sha256=Sg1pid83ofHGYhiUGyWDU1cUQShNuzqKR8-OdK1LwDU,114
14
+ msteams_transcripts-1.0.0.dist-info/licenses/LICENSE,sha256=rDxdB5fQvtRwfrjshCLNfl64Tp1Vu4RuVp1YsiwsuIw,1074
15
+ msteams_transcripts-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ msteams-transcripts = teams_transcripts.cli:main
3
+ teams-transcripts = teams_transcripts.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Divya Van Mahajan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,56 @@
1
+ """List and download Microsoft Teams meeting transcripts through a signed-in browser.
2
+
3
+ The Teams web client already receives the whole transcript when you open a meeting
4
+ Recap tab; the Download button is only a UI policy. This package attaches to a
5
+ browser you are signed in to and replays the same requests, so it sees exactly what
6
+ you can see and nothing more.
7
+
8
+ Library use::
9
+
10
+ import asyncio
11
+ from teams_transcripts import TeamsSession, download_ref, refs_from_recap_url
12
+
13
+ async def main():
14
+ async with TeamsSession() as s:
15
+ ref = refs_from_recap_url("https://teams.cloud.microsoft/l/meetingrecap?...")
16
+ await download_ref(s, ref, Path("out"), "txt", details=True)
17
+
18
+ asyncio.run(main())
19
+ """
20
+ from __future__ import annotations
21
+
22
+ __version__ = "1.0.0"
23
+
24
+ from .errors import TranscriptError
25
+
26
+ __all__ = [
27
+ "__version__",
28
+ "TranscriptError",
29
+ "TeamsSession",
30
+ "download_ref",
31
+ "refs_from_recap_url",
32
+ "transcript_refs_from_contents",
33
+ "transcript_to_text",
34
+ ]
35
+
36
+ _LAZY = {
37
+ "TeamsSession": ".session",
38
+ "download_ref": ".download",
39
+ "refs_from_recap_url": ".model",
40
+ "transcript_refs_from_contents": ".model",
41
+ "transcript_to_text": ".formats",
42
+ }
43
+
44
+
45
+ def __getattr__(name: str):
46
+ """Import the browser-driving parts only when they are actually used."""
47
+ module = _LAZY.get(name)
48
+ if module is None:
49
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
50
+ from importlib import import_module
51
+
52
+ return getattr(import_module(module, __name__), name)
53
+
54
+
55
+ def __dir__() -> list[str]:
56
+ return sorted(__all__)
@@ -0,0 +1,7 @@
1
+ """Allow `python -m teams_transcripts`."""
2
+ from __future__ import annotations
3
+
4
+ from .cli import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,73 @@
1
+ """Starting and finding the dedicated browser instance.
2
+
3
+ Recent Edge and Chrome refuse remote debugging on the user's normal profile, so
4
+ this always uses a separate profile directory. The user signs in there once;
5
+ corporate single sign-on usually does it without a prompt.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ import time
13
+ import urllib.request
14
+ from pathlib import Path
15
+
16
+ from .config import SETTINGS, TEAMS_URL, browser_candidates
17
+ from .errors import TranscriptError
18
+
19
+
20
+ def log(msg: str) -> None:
21
+ """Progress goes to stderr so that stdout stays machine-readable."""
22
+ print(msg, file=sys.stderr, flush=True)
23
+
24
+
25
+ def cdp_alive() -> bool:
26
+ try:
27
+ with urllib.request.urlopen(f"{SETTINGS.cdp_url}/json/version", timeout=2) as r:
28
+ return r.status == 200
29
+ except Exception:
30
+ return False
31
+
32
+
33
+ def find_browser() -> str:
34
+ exe = next((p for p in browser_candidates(SETTINGS.browser_exe) if Path(p).exists()), None)
35
+ if not exe:
36
+ raise TranscriptError(
37
+ "No Edge or Chrome executable found. Set TT_BROWSER_EXE or pass --browser with the full path."
38
+ )
39
+ return exe
40
+
41
+
42
+ def ensure_browser(wait_seconds: int = 30) -> None:
43
+ """Start the dedicated browser if it is not already listening on the debug port."""
44
+ if cdp_alive():
45
+ return
46
+ exe = find_browser()
47
+ SETTINGS.profile_dir.mkdir(parents=True, exist_ok=True)
48
+ log(f"Starting {Path(exe).name} with profile {SETTINGS.profile_dir} on port {SETTINGS.cdp_port} ...")
49
+ kwargs: dict = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, "close_fds": True}
50
+ if os.name == "nt":
51
+ kwargs["creationflags"] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
52
+ else:
53
+ kwargs["start_new_session"] = True
54
+ subprocess.Popen(
55
+ [
56
+ exe,
57
+ f"--user-data-dir={SETTINGS.profile_dir}",
58
+ f"--remote-debugging-port={SETTINGS.cdp_port}",
59
+ "--no-first-run",
60
+ "--no-default-browser-check",
61
+ TEAMS_URL,
62
+ ],
63
+ **kwargs,
64
+ )
65
+ for _ in range(wait_seconds):
66
+ time.sleep(1)
67
+ if cdp_alive():
68
+ log("Browser is up. If Teams asks you to sign in, do it in that window.")
69
+ return
70
+ raise TranscriptError(
71
+ "The browser did not expose its debugging port. "
72
+ f"Close any window already using the profile at {SETTINGS.profile_dir}, then try again."
73
+ )