indent 0.0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of indent might be problematic. Click here for more details.

Files changed (56) hide show
  1. exponent/__init__.py +1 -0
  2. exponent/cli.py +112 -0
  3. exponent/commands/cloud_commands.py +85 -0
  4. exponent/commands/common.py +434 -0
  5. exponent/commands/config_commands.py +581 -0
  6. exponent/commands/github_app_commands.py +211 -0
  7. exponent/commands/listen_commands.py +96 -0
  8. exponent/commands/run_commands.py +208 -0
  9. exponent/commands/settings.py +56 -0
  10. exponent/commands/shell_commands.py +2840 -0
  11. exponent/commands/theme.py +246 -0
  12. exponent/commands/types.py +111 -0
  13. exponent/commands/upgrade.py +29 -0
  14. exponent/commands/utils.py +236 -0
  15. exponent/core/config.py +180 -0
  16. exponent/core/graphql/__init__.py +0 -0
  17. exponent/core/graphql/client.py +59 -0
  18. exponent/core/graphql/cloud_config_queries.py +77 -0
  19. exponent/core/graphql/get_chats_query.py +47 -0
  20. exponent/core/graphql/github_config_queries.py +56 -0
  21. exponent/core/graphql/mutations.py +75 -0
  22. exponent/core/graphql/queries.py +110 -0
  23. exponent/core/graphql/subscriptions.py +452 -0
  24. exponent/core/remote_execution/checkpoints.py +212 -0
  25. exponent/core/remote_execution/cli_rpc_types.py +214 -0
  26. exponent/core/remote_execution/client.py +545 -0
  27. exponent/core/remote_execution/code_execution.py +58 -0
  28. exponent/core/remote_execution/command_execution.py +105 -0
  29. exponent/core/remote_execution/error_info.py +45 -0
  30. exponent/core/remote_execution/exceptions.py +10 -0
  31. exponent/core/remote_execution/file_write.py +410 -0
  32. exponent/core/remote_execution/files.py +415 -0
  33. exponent/core/remote_execution/git.py +268 -0
  34. exponent/core/remote_execution/languages/python_execution.py +239 -0
  35. exponent/core/remote_execution/languages/shell_streaming.py +221 -0
  36. exponent/core/remote_execution/languages/types.py +20 -0
  37. exponent/core/remote_execution/session.py +128 -0
  38. exponent/core/remote_execution/system_context.py +54 -0
  39. exponent/core/remote_execution/tool_execution.py +289 -0
  40. exponent/core/remote_execution/truncation.py +284 -0
  41. exponent/core/remote_execution/types.py +670 -0
  42. exponent/core/remote_execution/utils.py +600 -0
  43. exponent/core/types/__init__.py +0 -0
  44. exponent/core/types/command_data.py +206 -0
  45. exponent/core/types/event_types.py +89 -0
  46. exponent/core/types/generated/__init__.py +0 -0
  47. exponent/core/types/generated/strategy_info.py +225 -0
  48. exponent/migration-docs/login.md +112 -0
  49. exponent/py.typed +4 -0
  50. exponent/utils/__init__.py +0 -0
  51. exponent/utils/colors.py +92 -0
  52. exponent/utils/version.py +289 -0
  53. indent-0.0.8.dist-info/METADATA +36 -0
  54. indent-0.0.8.dist-info/RECORD +56 -0
  55. indent-0.0.8.dist-info/WHEEL +4 -0
  56. indent-0.0.8.dist-info/entry_points.txt +2 -0
exponent/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.8" # Keep in sync with pyproject.toml
exponent/cli.py ADDED
@@ -0,0 +1,112 @@
1
+ import click
2
+ from click import Context, HelpFormatter
3
+
4
+ from exponent.commands.cloud_commands import cloud_cli
5
+ from exponent.commands.common import set_log_level
6
+ from exponent.commands.config_commands import config_cli
7
+ from exponent.commands.github_app_commands import github_app_cli
8
+ from exponent.commands.listen_commands import listen_cli
9
+ from exponent.commands.run_commands import run_cli
10
+ from exponent.commands.shell_commands import shell_cli
11
+ from exponent.commands.types import ExponentGroup, exponent_cli_group
12
+ from exponent.commands.upgrade import upgrade_cli
13
+ from exponent.utils.version import (
14
+ get_installed_version,
15
+ )
16
+
17
+
18
+ @exponent_cli_group()
19
+ @click.version_option(get_installed_version(), prog_name="Indent CLI")
20
+ @click.pass_context
21
+ def cli(ctx: Context) -> None:
22
+ """
23
+ Indent: Your AI pair programmer.
24
+
25
+ Run indent run to start
26
+ """
27
+ set_log_level()
28
+
29
+
30
+ sources: list[ExponentGroup] = [
31
+ config_cli, # Configuration commands
32
+ run_cli, # Run AI chat commands
33
+ shell_cli, # Shell interaction commands
34
+ upgrade_cli, # Upgrade-related commands
35
+ cloud_cli, # Cloud commands
36
+ listen_cli, # Listen to chat events
37
+ github_app_cli, # Setup github app installation
38
+ ]
39
+
40
+ for source in sources:
41
+ for command in source.commands.values():
42
+ cli.add_command(command)
43
+
44
+
45
+ def format_commands(
46
+ ctx: Context, formatter: HelpFormatter, include_hidden: bool = False
47
+ ) -> None:
48
+ commands = []
49
+ hidden_commands = []
50
+ for cmd_name in cli.list_commands(ctx):
51
+ cmd = cli.get_command(ctx, cmd_name)
52
+ if cmd is None:
53
+ continue
54
+ if cmd.hidden:
55
+ hidden_commands.append((cmd_name, cmd))
56
+ else:
57
+ commands.append((cmd_name, cmd))
58
+
59
+ if commands:
60
+ max_cmd_length = (
61
+ max(len(cmd_name) for cmd_name, _ in commands) if commands else 0
62
+ )
63
+ limit = (
64
+ (formatter.width or 80) - 6 - max_cmd_length
65
+ ) # Default width to 80 if None
66
+ rows = []
67
+ for cmd_name, cmd in commands:
68
+ help_text = cmd.get_short_help_str(limit)
69
+ rows.append((cmd_name, help_text))
70
+
71
+ with formatter.section("Commands"):
72
+ formatter.write_dl(rows)
73
+
74
+ if include_hidden and hidden_commands:
75
+ max_cmd_length = (
76
+ max(len(cmd_name) for cmd_name, _ in hidden_commands)
77
+ if hidden_commands
78
+ else 0
79
+ )
80
+ limit = (
81
+ (formatter.width or 80) - 6 - max_cmd_length
82
+ ) # Default width to 80 if None
83
+ hidden_rows = []
84
+ for cmd_name, cmd in hidden_commands:
85
+ help_text = cmd.get_short_help_str(limit)
86
+ hidden_rows.append((cmd_name, help_text))
87
+
88
+ with formatter.section("Hidden Commands"):
89
+ formatter.write_dl(hidden_rows)
90
+
91
+
92
+ @cli.command(hidden=True)
93
+ @click.pass_context
94
+ def hidden(ctx: Context) -> None:
95
+ """Show all commands, including hidden ones."""
96
+ formatter = ctx.make_formatter()
97
+ with formatter.section("Usage"):
98
+ if ctx.parent and ctx.parent.command:
99
+ formatter.write_usage(
100
+ ctx.parent.command.name or "exponent", "COMMAND [ARGS]..."
101
+ )
102
+ formatter.write_paragraph()
103
+ with formatter.indentation():
104
+ if cli.help:
105
+ formatter.write_text(cli.help)
106
+ formatter.write_paragraph()
107
+ format_commands(ctx, formatter, include_hidden=True)
108
+ click.echo(formatter.getvalue().rstrip("\n"))
109
+
110
+
111
+ if __name__ == "__main__":
112
+ cli()
@@ -0,0 +1,85 @@
1
+ import asyncio
2
+ import sys
3
+
4
+ import click
5
+
6
+ from exponent.commands.common import (
7
+ check_inside_git_repo,
8
+ check_running_from_home_directory,
9
+ check_ssl,
10
+ create_cloud_chat,
11
+ redirect_to_login,
12
+ start_chat_turn,
13
+ )
14
+ from exponent.commands.settings import use_settings
15
+ from exponent.commands.types import exponent_cli_group
16
+ from exponent.commands.utils import (
17
+ launch_exponent_browser,
18
+ print_exponent_message,
19
+ )
20
+ from exponent.core.config import Settings
21
+ from exponent.utils.version import check_exponent_version_and_upgrade
22
+
23
+
24
+ @exponent_cli_group(hidden=True)
25
+ def cloud_cli() -> None:
26
+ pass
27
+
28
+
29
+ @cloud_cli.command(hidden=True)
30
+ @click.option(
31
+ "--cloud-config-id",
32
+ help="ID of an existing cloud config to reconnect",
33
+ required=True,
34
+ )
35
+ @click.option(
36
+ "--prompt",
37
+ help="Prompt to kick off the cloud session.",
38
+ required=True,
39
+ )
40
+ @click.option(
41
+ "--background",
42
+ "-b",
43
+ help="Start the cloud session without launching the Exponent UI",
44
+ is_flag=True,
45
+ default=False,
46
+ )
47
+ @use_settings
48
+ def cloud(
49
+ settings: Settings,
50
+ cloud_config_id: str,
51
+ prompt: str,
52
+ background: bool,
53
+ ) -> None:
54
+ check_exponent_version_and_upgrade(settings)
55
+
56
+ if not settings.api_key:
57
+ redirect_to_login(settings)
58
+ return
59
+
60
+ loop = asyncio.get_event_loop()
61
+
62
+ check_running_from_home_directory()
63
+ loop.run_until_complete(check_inside_git_repo(settings))
64
+ check_ssl()
65
+
66
+ api_key = settings.api_key
67
+ base_url = settings.base_url
68
+ base_api_url = settings.get_base_api_url()
69
+ base_ws_url = settings.get_base_ws_url()
70
+
71
+ chat_uuid = loop.run_until_complete(
72
+ create_cloud_chat(api_key, base_api_url, base_ws_url, cloud_config_id)
73
+ )
74
+
75
+ if chat_uuid is None:
76
+ sys.exit(1)
77
+
78
+ loop.run_until_complete(
79
+ start_chat_turn(api_key, base_api_url, base_ws_url, chat_uuid, prompt)
80
+ )
81
+
82
+ print_exponent_message(base_url, chat_uuid)
83
+
84
+ if not background:
85
+ launch_exponent_browser(settings.environment, base_url, chat_uuid)
@@ -0,0 +1,434 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import os.path
5
+ import platform
6
+ import ssl
7
+ import stat
8
+ import sys
9
+ import webbrowser
10
+ from collections.abc import Coroutine
11
+ from typing import Any, cast
12
+
13
+ import certifi
14
+ import click
15
+ import httpx
16
+ from dotenv import load_dotenv
17
+
18
+ from exponent.commands.utils import ConnectionTracker
19
+ from exponent.core.config import (
20
+ Settings,
21
+ get_settings,
22
+ )
23
+ from exponent.core.graphql.client import GraphQLClient
24
+ from exponent.core.graphql.mutations import (
25
+ CREATE_CLOUD_CHAT_MUTATION,
26
+ REFRESH_API_KEY_MUTATION,
27
+ SET_LOGIN_COMPLETE_MUTATION,
28
+ START_CHAT_TURN_MUTATION,
29
+ )
30
+ from exponent.core.remote_execution.client import (
31
+ REMOTE_EXECUTION_CLIENT_EXIT_INFO,
32
+ RemoteExecutionClient,
33
+ )
34
+ from exponent.core.remote_execution.exceptions import (
35
+ ExponentError,
36
+ HandledExponentError,
37
+ )
38
+ from exponent.core.remote_execution.files import FileCache
39
+ from exponent.core.remote_execution.git import get_git_info
40
+ from exponent.core.remote_execution.types import ChatSource, GitInfo
41
+
42
+ load_dotenv()
43
+
44
+
45
+ def set_log_level() -> None:
46
+ settings = get_settings()
47
+ logging.basicConfig(level=getattr(logging, settings.log_level), stream=sys.stdout)
48
+
49
+
50
+ def redirect_to_login(settings: Settings, cause: str = "detected") -> None:
51
+ if inside_ssh_session():
52
+ click.echo(f"No API Key {cause}, run 'indent login --key <API-KEY>'")
53
+ else:
54
+ click.echo("No API Key detected, redirecting to login...")
55
+ webbrowser.open(f"{settings.base_url}/settings")
56
+
57
+
58
+ def inside_ssh_session() -> bool:
59
+ return (os.environ.get("SSH_TTY") or os.environ.get("SSH_TTY")) is not None
60
+
61
+
62
+ async def inside_git_repo() -> bool:
63
+ git_info = await get_git_info(os.getcwd())
64
+
65
+ return git_info is not None
66
+
67
+
68
+ def missing_ssl_certs() -> bool:
69
+ if platform.system().lower() != "darwin":
70
+ return False
71
+
72
+ openssl_dir, openssl_cafile = os.path.split(
73
+ ssl.get_default_verify_paths().openssl_cafile
74
+ )
75
+
76
+ return not os.path.exists(os.path.join(openssl_dir, openssl_cafile))
77
+
78
+
79
+ def install_ssl_certs() -> None:
80
+ STAT_0o775 = (
81
+ stat.S_IRUSR
82
+ | stat.S_IWUSR
83
+ | stat.S_IXUSR
84
+ | stat.S_IRGRP
85
+ | stat.S_IWGRP
86
+ | stat.S_IXGRP
87
+ | stat.S_IROTH
88
+ | stat.S_IXOTH
89
+ )
90
+
91
+ openssl_dir, openssl_cafile = os.path.split(
92
+ ssl.get_default_verify_paths().openssl_cafile
93
+ )
94
+
95
+ cwd = os.getcwd()
96
+ # change working directory to the default SSL directory
97
+ os.chdir(openssl_dir)
98
+ relpath_to_certifi_cafile = os.path.relpath(certifi.where())
99
+
100
+ try:
101
+ os.remove(openssl_cafile)
102
+ except FileNotFoundError:
103
+ pass
104
+
105
+ click.echo(" -- creating symlink to certifi certificate bundle")
106
+ os.symlink(relpath_to_certifi_cafile, openssl_cafile)
107
+ click.echo(" -- setting permissions")
108
+ os.chmod(openssl_cafile, STAT_0o775)
109
+ click.echo(" -- update complete")
110
+ os.chdir(cwd)
111
+
112
+
113
+ def check_ssl() -> None:
114
+ if missing_ssl_certs():
115
+ click.confirm(
116
+ "Missing root SSL certs required for python to make HTTP requests, "
117
+ "install certifi certificates now?",
118
+ abort=True,
119
+ default=True,
120
+ )
121
+
122
+ install_ssl_certs()
123
+
124
+
125
+ async def check_inside_git_repo(settings: Settings) -> None:
126
+ if not settings.options.git_warning_disabled and not (await inside_git_repo()):
127
+ click.echo(
128
+ click.style(
129
+ "\nWarning: Running from a folder that is not a git repository",
130
+ fg="yellow",
131
+ bold=True,
132
+ )
133
+ )
134
+ click.echo(
135
+ "This is a check to make sure you are running Exponent from the root of your project."
136
+ )
137
+
138
+ click.echo(f"\nCurrent directory: {click.style(os.getcwd(), fg='cyan')}")
139
+
140
+ click.echo("\nRecommendation:")
141
+ click.echo(" Run Exponent from the root directory of your codebase.")
142
+ click.echo("\nExample:")
143
+ click.echo(
144
+ f" If your project is in {click.style('~/my-project', fg='cyan')}, run:"
145
+ )
146
+ click.echo(f" {click.style('cd ~/my-project && exponent run', fg='green')}")
147
+
148
+ # Tell the user they can run exponent config --no-git-warning to disable this check
149
+ click.echo(
150
+ f"\nYou can run {click.style('exponent config --set-git-warning-disabled', fg='green')} to disable this check."
151
+ )
152
+
153
+ if not click.confirm(
154
+ click.style(
155
+ f"\nDo you want to continue running Exponent from {os.getcwd()}?",
156
+ fg="yellow",
157
+ ),
158
+ default=True,
159
+ ):
160
+ click.echo(click.style("\nOperation aborted.", fg="red"))
161
+ raise click.Abort()
162
+
163
+
164
+ def check_running_from_home_directory(require_confirmation: bool = True) -> bool:
165
+ if os.path.expanduser("~") == os.getcwd():
166
+ click.echo(
167
+ click.style(
168
+ "\nWarning: Running Exponent from Home Directory",
169
+ fg="yellow",
170
+ bold=True,
171
+ )
172
+ )
173
+ click.echo(
174
+ "Running Exponent from your home directory can cause unexpected issues."
175
+ )
176
+ click.echo("\nRecommendation:")
177
+ click.echo(" Run Exponent from the root directory of your codebase.")
178
+ click.echo("\nExample:")
179
+ click.echo(
180
+ f" If your project is in {click.style('~/my-project', fg='cyan')}, run:"
181
+ )
182
+ click.echo(f" {click.style('cd ~/my-project && exponent run', fg='green')}")
183
+
184
+ if require_confirmation:
185
+ if not click.confirm(
186
+ click.style(
187
+ f"\nDo you want to continue running Exponent from {os.getcwd()}?",
188
+ fg="yellow",
189
+ ),
190
+ default=True,
191
+ ):
192
+ click.echo(click.style("\nOperation aborted.", fg="red"))
193
+ raise click.Abort()
194
+ else:
195
+ click.echo("\n") # Newline to separate from next command
196
+
197
+ return True
198
+
199
+ return False
200
+
201
+
202
+ def run_until_complete(coro: Coroutine[Any, Any, Any]) -> Any:
203
+ loop = asyncio.get_event_loop()
204
+ task = loop.create_task(coro)
205
+
206
+ try:
207
+ loop.run_until_complete(task)
208
+ except KeyboardInterrupt:
209
+ task.cancel()
210
+ try:
211
+ loop.run_until_complete(task)
212
+ except asyncio.CancelledError:
213
+ pass
214
+ except ExponentError as e:
215
+ click.secho(f"Encountered error: {e}", fg="red")
216
+ click.secho(
217
+ "The Exponent team has been notified, "
218
+ "please try again and reach out if the problem persists.",
219
+ fg="yellow",
220
+ )
221
+ sys.exit(1)
222
+ except HandledExponentError as e:
223
+ click.secho(str(e), fg="red")
224
+ sys.exit(1)
225
+
226
+
227
+ async def run_client_connection(
228
+ client: RemoteExecutionClient,
229
+ chat_uuid: str,
230
+ connection_tracker: ConnectionTracker | None = None,
231
+ ) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO:
232
+ return await client.run_connection(chat_uuid, connection_tracker)
233
+
234
+
235
+ async def create_cloud_chat(
236
+ api_key: str, base_api_url: str, base_ws_url: str, config_uuid: str
237
+ ) -> str:
238
+ graphql_client = GraphQLClient(
239
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
240
+ )
241
+
242
+ variables = {
243
+ "configId": config_uuid,
244
+ }
245
+
246
+ result = await graphql_client.execute(
247
+ CREATE_CLOUD_CHAT_MUTATION, variables, "CreateCloudChat", timeout=120
248
+ )
249
+
250
+ data = result["createCloudChat"]
251
+
252
+ if data["__typename"] != "Chat":
253
+ raise HandledExponentError(f"Error creating cloud chat: {data['message']}")
254
+
255
+ return str(data["chatUuid"])
256
+
257
+
258
+ async def start_chat_turn(
259
+ api_key: str, base_api_url: str, base_ws_url: str, chat_uuid: str, prompt: str
260
+ ) -> None:
261
+ graphql_client = GraphQLClient(
262
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
263
+ )
264
+
265
+ variables = {
266
+ "chatInput": {"prompt": {"message": prompt, "attachments": []}},
267
+ "parentUuid": None,
268
+ "chatConfig": {
269
+ "chatUuid": chat_uuid,
270
+ "exponentModel": "PREMIUM",
271
+ "requireConfirmation": False,
272
+ "readOnly": False,
273
+ "depthLimit": 20,
274
+ },
275
+ }
276
+ result = await graphql_client.execute(
277
+ START_CHAT_TURN_MUTATION, variables, "StartChatTurnMutation"
278
+ )
279
+
280
+ data = result["startChatReply"]
281
+
282
+ if data["__typename"] != "Chat":
283
+ raise HandledExponentError(f"Error starting chat turn: {data['message']}")
284
+
285
+
286
+ async def run_workflow(
287
+ base_url: str,
288
+ client: RemoteExecutionClient,
289
+ chat_uuid: str,
290
+ workflow_id: str,
291
+ ) -> None:
292
+ click.secho("Running workflow...")
293
+ workflow_data = await client.run_workflow(chat_uuid, workflow_id)
294
+ click.secho("Workflow started.")
295
+ if workflow_data and "workflow_run_uuid" in workflow_data:
296
+ click.echo(
297
+ " - Link: "
298
+ + click.style(
299
+ f"{base_url}/workflow/{workflow_data['workflow_run_uuid']}",
300
+ fg=(100, 200, 255),
301
+ )
302
+ )
303
+
304
+
305
+ async def start_client(
306
+ api_key: str,
307
+ base_url: str,
308
+ base_api_url: str,
309
+ base_ws_url: str,
310
+ chat_uuid: str,
311
+ file_cache: FileCache | None = None,
312
+ prompt: str | None = None,
313
+ workflow_id: str | None = None,
314
+ connection_tracker: ConnectionTracker | None = None,
315
+ ) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO:
316
+ async with RemoteExecutionClient.session(
317
+ api_key=api_key,
318
+ base_url=base_api_url,
319
+ base_ws_url=base_ws_url,
320
+ working_directory=os.getcwd(),
321
+ file_cache=file_cache,
322
+ ) as client:
323
+ main_coro = run_client_connection(client, chat_uuid, connection_tracker)
324
+ aux_coros: list[Coroutine[Any, Any, None]] = []
325
+
326
+ if prompt:
327
+ # If given a prompt, we also need to send a request
328
+ # to kick off the initial turn loop for the chat
329
+ raise NotImplementedError("Kicking off with initial prompt not implemented")
330
+ elif workflow_id:
331
+ # Similarly, if given a workflow ID, we need to send
332
+ # a request to kick off the workflow
333
+ aux_coros.append(run_workflow(base_url, client, chat_uuid, workflow_id))
334
+
335
+ client_result, *_ = await asyncio.gather(main_coro, *aux_coros)
336
+ return cast(REMOTE_EXECUTION_CLIENT_EXIT_INFO, client_result)
337
+
338
+
339
+ # Helper functions
340
+ async def create_chat(
341
+ api_key: str, base_api_url: str, base_ws_url: str, chat_source: ChatSource
342
+ ) -> str | None:
343
+ try:
344
+ async with RemoteExecutionClient.session(
345
+ api_key, base_api_url, base_ws_url, os.getcwd()
346
+ ) as client:
347
+ chat = await client.create_chat(chat_source)
348
+ return chat.chat_uuid
349
+ except (httpx.ConnectError, ExponentError) as e:
350
+ click.secho(f"Error: {e}", fg="red")
351
+ return None
352
+
353
+
354
+ async def get_gh_app_installation_token(
355
+ api_key: str, base_api_url: str, base_ws_url: str, git_info: GitInfo
356
+ ) -> dict[str, Any] | None:
357
+ try:
358
+ async with RemoteExecutionClient.session(
359
+ api_key, base_api_url, base_ws_url, os.getcwd()
360
+ ) as client:
361
+ return await client.get_gh_installation_token(git_info)
362
+ except (httpx.ConnectError, ExponentError) as e:
363
+ click.secho(f"Error: {e}", fg="red")
364
+ return None
365
+
366
+
367
+ async def verify_gh_app_installation(
368
+ api_key: str, base_api_url: str, base_ws_url: str, git_info: GitInfo
369
+ ) -> bool:
370
+ try:
371
+ async with RemoteExecutionClient.session(
372
+ api_key, base_api_url, base_ws_url, os.getcwd()
373
+ ) as client:
374
+ res = await client.get_gh_installation_token(git_info)
375
+ if "token" in res:
376
+ return True
377
+ except (httpx.ConnectError, ExponentError) as e:
378
+ click.secho(f"Error: {e}", fg="red")
379
+ return False
380
+
381
+
382
+ async def set_login_complete(api_key: str, base_api_url: str, base_ws_url: str) -> None:
383
+ graphql_client = GraphQLClient(
384
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
385
+ )
386
+ result = await graphql_client.execute(
387
+ SET_LOGIN_COMPLETE_MUTATION, {}, "SetLoginComplete"
388
+ )
389
+
390
+ data = result["setLoginComplete"]
391
+
392
+ if data["__typename"] == "UnauthenticatedError":
393
+ raise HandledExponentError(f"Verification failed: {data['message']}")
394
+
395
+ if data["userApiKey"] != api_key:
396
+ # We got a user object back, but the api_key is different
397
+ # than the one used in the user's request...
398
+ # This should never happen
399
+ raise HandledExponentError(
400
+ "Invalid API key, login to https://exponent.run to find your API key."
401
+ )
402
+
403
+
404
+ async def refresh_api_key_task(
405
+ api_key: str,
406
+ base_api_url: str,
407
+ base_ws_url: str,
408
+ ) -> None:
409
+ graphql_client = GraphQLClient(api_key, base_api_url, base_ws_url)
410
+ result = await graphql_client.execute(REFRESH_API_KEY_MUTATION)
411
+
412
+ if "refreshApiKey" in result:
413
+ if "message" in result["refreshApiKey"]:
414
+ # Handle error case
415
+ click.secho(f"Error: {result['refreshApiKey']['message']}", fg="red")
416
+ return
417
+
418
+ if "userApiKey" in result["refreshApiKey"]:
419
+ # Handle success case
420
+ new_api_key = result["refreshApiKey"]["userApiKey"]
421
+ settings = get_settings()
422
+
423
+ click.echo(f"Saving new API Key to {settings.config_file_path}")
424
+ settings.update_api_key(new_api_key)
425
+ settings.write_settings_to_config_file()
426
+
427
+ click.secho(
428
+ "API key has been refreshed and saved successfully!", fg="green"
429
+ )
430
+ return
431
+
432
+ # Handle unexpected response
433
+ click.secho("Failed to refresh API key: Unexpected response", fg="red")
434
+ click.echo(result)