indent 0.1.11__tar.gz → 0.1.13__tar.gz

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.
Files changed (54) hide show
  1. {indent-0.1.11 → indent-0.1.13}/.gitignore +1 -0
  2. {indent-0.1.11 → indent-0.1.13}/PKG-INFO +3 -3
  3. {indent-0.1.11 → indent-0.1.13}/exponent/__init__.py +2 -2
  4. indent-0.1.13/exponent/commands/cloud_commands.py +667 -0
  5. {indent-0.1.11 → indent-0.1.13}/exponent/commands/common.py +4 -9
  6. {indent-0.1.11 → indent-0.1.13}/exponent/commands/run_commands.py +20 -9
  7. {indent-0.1.11 → indent-0.1.13}/exponent/commands/workflow_commands.py +1 -1
  8. {indent-0.1.11 → indent-0.1.13}/exponent/core/graphql/client.py +5 -3
  9. indent-0.1.13/exponent/core/graphql/mutations.py +189 -0
  10. {indent-0.1.11 → indent-0.1.13}/exponent/core/graphql/queries.py +23 -0
  11. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/cli_rpc_types.py +17 -0
  12. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/client.py +108 -19
  13. indent-0.1.13/exponent/core/remote_execution/file_write.py +35 -0
  14. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/types.py +1 -7
  15. {indent-0.1.11 → indent-0.1.13}/exponent/core/types/generated/strategy_info.py +0 -12
  16. {indent-0.1.11 → indent-0.1.13}/pyproject.toml +3 -2
  17. indent-0.1.11/exponent/commands/cloud_commands.py +0 -85
  18. indent-0.1.11/exponent/core/graphql/mutations.py +0 -75
  19. indent-0.1.11/exponent/core/remote_execution/file_write.py +0 -410
  20. {indent-0.1.11 → indent-0.1.13}/exponent/cli.py +0 -0
  21. {indent-0.1.11 → indent-0.1.13}/exponent/commands/config_commands.py +0 -0
  22. {indent-0.1.11 → indent-0.1.13}/exponent/commands/settings.py +0 -0
  23. {indent-0.1.11 → indent-0.1.13}/exponent/commands/types.py +0 -0
  24. {indent-0.1.11 → indent-0.1.13}/exponent/commands/upgrade.py +0 -0
  25. {indent-0.1.11 → indent-0.1.13}/exponent/commands/utils.py +0 -0
  26. {indent-0.1.11 → indent-0.1.13}/exponent/core/config.py +0 -0
  27. {indent-0.1.11 → indent-0.1.13}/exponent/core/graphql/__init__.py +0 -0
  28. {indent-0.1.11 → indent-0.1.13}/exponent/core/graphql/get_chats_query.py +0 -0
  29. {indent-0.1.11 → indent-0.1.13}/exponent/core/graphql/github_config_queries.py +0 -0
  30. {indent-0.1.11 → indent-0.1.13}/exponent/core/graphql/subscriptions.py +0 -0
  31. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/checkpoints.py +0 -0
  32. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/code_execution.py +0 -0
  33. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/error_info.py +0 -0
  34. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/exceptions.py +0 -0
  35. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/files.py +0 -0
  36. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/git.py +0 -0
  37. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/http_fetch.py +0 -0
  38. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/languages/python_execution.py +0 -0
  39. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/languages/shell_streaming.py +0 -0
  40. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/languages/types.py +0 -0
  41. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/session.py +0 -0
  42. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/system_context.py +0 -0
  43. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/tool_execution.py +0 -0
  44. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/truncation.py +0 -0
  45. {indent-0.1.11 → indent-0.1.13}/exponent/core/remote_execution/utils.py +0 -0
  46. {indent-0.1.11 → indent-0.1.13}/exponent/core/types/__init__.py +0 -0
  47. {indent-0.1.11 → indent-0.1.13}/exponent/core/types/command_data.py +0 -0
  48. {indent-0.1.11 → indent-0.1.13}/exponent/core/types/event_types.py +0 -0
  49. {indent-0.1.11 → indent-0.1.13}/exponent/core/types/generated/__init__.py +0 -0
  50. {indent-0.1.11 → indent-0.1.13}/exponent/migration-docs/login.md +0 -0
  51. {indent-0.1.11 → indent-0.1.13}/exponent/py.typed +0 -0
  52. {indent-0.1.11 → indent-0.1.13}/exponent/utils/__init__.py +0 -0
  53. {indent-0.1.11 → indent-0.1.13}/exponent/utils/colors.py +0 -0
  54. {indent-0.1.11 → indent-0.1.13}/exponent/utils/version.py +0 -0
@@ -30,6 +30,7 @@ lib/
30
30
  lib64/
31
31
  parts/
32
32
  sdist/
33
+ temp/
33
34
  var/
34
35
  wheels/
35
36
  pip-wheel-metadata/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: indent
3
- Version: 0.1.11
3
+ Version: 0.1.13
4
4
  Summary: Indent is an AI Pair Programmer
5
5
  Author-email: Sashank Thupukari <sashank@exponent.run>
6
6
  Requires-Python: <3.13,>=3.10
@@ -14,7 +14,7 @@ Requires-Dist: diff-match-patch<20230431,>=20230430
14
14
  Requires-Dist: eval-type-backport<0.3,>=0.2.0
15
15
  Requires-Dist: git-python>=1.0.3
16
16
  Requires-Dist: gitignore-parser<0.2,>=0.1.11
17
- Requires-Dist: gql[httpx,websockets]<4,>=3.5.0
17
+ Requires-Dist: gql[httpx,websockets]~=4.0
18
18
  Requires-Dist: httpx>=0.28.1
19
19
  Requires-Dist: ipykernel<7,>=6.29.4
20
20
  Requires-Dist: jupyter-client<9,>=8.6.1
@@ -32,4 +32,4 @@ Requires-Dist: rapidfuzz<4,>=3.9.0
32
32
  Requires-Dist: rich<14,>=13.7.1
33
33
  Requires-Dist: sentry-sdk<3,>=2.1.1
34
34
  Requires-Dist: toml<0.11,>=0.10.2
35
- Requires-Dist: websockets~=11.0
35
+ Requires-Dist: websockets~=15.0
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.11'
32
- __version_tuple__ = version_tuple = (0, 1, 11)
31
+ __version__ = version = '0.1.13'
32
+ __version_tuple__ = version_tuple = (0, 1, 13)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -0,0 +1,667 @@
1
+ import asyncio
2
+ import sys
3
+ from typing import Any, cast
4
+
5
+ import click
6
+
7
+ from exponent.commands.common import (
8
+ check_inside_git_repo,
9
+ check_running_from_home_directory,
10
+ check_ssl,
11
+ create_cloud_chat,
12
+ redirect_to_login,
13
+ start_chat_turn,
14
+ )
15
+ from exponent.commands.settings import use_settings
16
+ from exponent.commands.types import exponent_cli_group
17
+ from exponent.commands.utils import (
18
+ launch_exponent_browser,
19
+ print_exponent_message,
20
+ )
21
+ from exponent.core.config import Settings
22
+ from exponent.core.graphql.client import GraphQLClient
23
+ from exponent.core.graphql.mutations import (
24
+ CREATE_CLOUD_CHAT_FROM_REPOSITORY_MUTATION,
25
+ ENABLE_CLOUD_REPOSITORY_MUTATION,
26
+ INCREMENTAL_BUILD_CLOUD_REPOSITORY_MUTATION,
27
+ REBUILD_CLOUD_REPOSITORY_MUTATION,
28
+ START_CHAT_TURN_MUTATION,
29
+ )
30
+ from exponent.core.graphql.queries import GITHUB_REPOSITORIES_QUERY
31
+ from exponent.utils.version import check_exponent_version_and_upgrade
32
+
33
+
34
+ @exponent_cli_group(hidden=True)
35
+ def cloud_cli() -> None:
36
+ pass
37
+
38
+
39
+ async def enable_cloud_repository(
40
+ api_key: str,
41
+ base_api_url: str,
42
+ base_ws_url: str,
43
+ org_name: str,
44
+ repo_name: str,
45
+ ) -> dict[str, Any]:
46
+ graphql_client = GraphQLClient(
47
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
48
+ )
49
+
50
+ variables = {
51
+ "orgName": org_name,
52
+ "repoName": repo_name,
53
+ }
54
+
55
+ result = await graphql_client.execute(
56
+ ENABLE_CLOUD_REPOSITORY_MUTATION,
57
+ variables,
58
+ "EnableCloudRepository",
59
+ timeout=120,
60
+ )
61
+
62
+ return cast(dict[str, Any], result["enableCloudRepository"])
63
+
64
+
65
+ async def incremental_build_cloud_repository(
66
+ api_key: str,
67
+ base_api_url: str,
68
+ base_ws_url: str,
69
+ org_name: str,
70
+ repo_name: str,
71
+ ) -> dict[str, Any]:
72
+ graphql_client = GraphQLClient(
73
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
74
+ )
75
+
76
+ variables = {
77
+ "orgName": org_name,
78
+ "repoName": repo_name,
79
+ }
80
+
81
+ result = await graphql_client.execute(
82
+ INCREMENTAL_BUILD_CLOUD_REPOSITORY_MUTATION,
83
+ variables,
84
+ "IncrementalBuildCloudRepository",
85
+ timeout=120,
86
+ )
87
+
88
+ return cast(dict[str, Any], result["incrementalBuildCloudRepository"])
89
+
90
+
91
+ async def rebuild_cloud_repository(
92
+ api_key: str,
93
+ base_api_url: str,
94
+ base_ws_url: str,
95
+ org_name: str,
96
+ repo_name: str,
97
+ ) -> dict[str, Any]:
98
+ graphql_client = GraphQLClient(
99
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
100
+ )
101
+
102
+ variables = {
103
+ "orgName": org_name,
104
+ "repoName": repo_name,
105
+ }
106
+
107
+ result = await graphql_client.execute(
108
+ REBUILD_CLOUD_REPOSITORY_MUTATION,
109
+ variables,
110
+ "RebuildCloudRepository",
111
+ timeout=120,
112
+ )
113
+
114
+ return cast(dict[str, Any], result["rebuildCloudRepository"])
115
+
116
+
117
+ async def list_github_repositories(
118
+ api_key: str,
119
+ base_api_url: str,
120
+ base_ws_url: str,
121
+ ) -> dict[str, Any]:
122
+ graphql_client = GraphQLClient(
123
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
124
+ )
125
+
126
+ result = await graphql_client.execute(
127
+ GITHUB_REPOSITORIES_QUERY,
128
+ None,
129
+ "GithubRepositories",
130
+ timeout=120,
131
+ )
132
+
133
+ return cast(dict[str, Any], result["githubRepositories"])
134
+
135
+
136
+ async def create_cloud_chat_from_repository(
137
+ api_key: str,
138
+ base_api_url: str,
139
+ base_ws_url: str,
140
+ repository_id: str,
141
+ ) -> dict[str, Any]:
142
+ graphql_client = GraphQLClient(
143
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
144
+ )
145
+
146
+ variables = {
147
+ "repositoryId": repository_id,
148
+ }
149
+
150
+ result = await graphql_client.execute(
151
+ CREATE_CLOUD_CHAT_FROM_REPOSITORY_MUTATION,
152
+ variables,
153
+ "CreateCloudChatFromRepository",
154
+ timeout=120,
155
+ )
156
+
157
+ return cast(dict[str, Any], result["createCloudChat"])
158
+
159
+
160
+ async def start_chat_turn_with_prompt(
161
+ api_key: str,
162
+ base_api_url: str,
163
+ base_ws_url: str,
164
+ chat_uuid: str,
165
+ prompt: str,
166
+ ) -> dict[str, Any]:
167
+ graphql_client = GraphQLClient(
168
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
169
+ )
170
+
171
+ variables = {
172
+ "chatInput": {"prompt": {"message": prompt, "attachments": []}},
173
+ "parentUuid": None,
174
+ "chatConfig": {
175
+ "chatUuid": chat_uuid,
176
+ "exponentModel": "PREMIUM",
177
+ "requireConfirmation": False,
178
+ "readOnly": False,
179
+ "depthLimit": 20,
180
+ },
181
+ }
182
+
183
+ result = await graphql_client.execute(
184
+ START_CHAT_TURN_MUTATION, variables, "StartChatTurnMutation", timeout=120
185
+ )
186
+
187
+ return cast(dict[str, Any], result["startChatReply"])
188
+
189
+
190
+ @cloud_cli.command(hidden=True)
191
+ @click.option(
192
+ "--org-name",
193
+ help="GitHub organization name",
194
+ required=True,
195
+ )
196
+ @click.option(
197
+ "--repo-name",
198
+ help="GitHub repository name",
199
+ required=True,
200
+ )
201
+ @use_settings
202
+ def enable_repo(
203
+ settings: Settings,
204
+ org_name: str,
205
+ repo_name: str,
206
+ ) -> None:
207
+ """Test utility for enabling cloud repository."""
208
+ check_exponent_version_and_upgrade(settings)
209
+
210
+ if not settings.api_key:
211
+ redirect_to_login(settings)
212
+ return
213
+
214
+ loop = asyncio.get_event_loop()
215
+
216
+ api_key = settings.api_key
217
+ base_api_url = settings.get_base_api_url()
218
+ base_ws_url = settings.get_base_ws_url()
219
+
220
+ try:
221
+ result = loop.run_until_complete(
222
+ enable_cloud_repository(
223
+ api_key, base_api_url, base_ws_url, org_name, repo_name
224
+ )
225
+ )
226
+
227
+ if result["__typename"] == "ContainerImage":
228
+ click.secho(
229
+ f"✓ Successfully enabled repository {org_name}/{repo_name}", fg="green"
230
+ )
231
+ click.echo(f" Build ref: {result.get('buildRef', 'N/A')}")
232
+ click.echo(f" Created at: {result.get('createdAt', 'N/A')}")
233
+ click.echo(f" Updated at: {result.get('updatedAt', 'N/A')}")
234
+ else:
235
+ click.secho(
236
+ f"✗ Failed to enable repository: {result.get('message', 'Unknown error')}",
237
+ fg="red",
238
+ )
239
+ click.echo(f" Error type: {result['__typename']}")
240
+
241
+ except Exception as e:
242
+ click.secho(f"✗ Error enabling repository: {e!s}", fg="red")
243
+ sys.exit(1)
244
+
245
+
246
+ @cloud_cli.command(hidden=True)
247
+ @click.option(
248
+ "--org-name",
249
+ help="GitHub organization name",
250
+ required=True,
251
+ )
252
+ @click.option(
253
+ "--repo-name",
254
+ help="GitHub repository name",
255
+ required=True,
256
+ )
257
+ @use_settings
258
+ def incremental_build(
259
+ settings: Settings,
260
+ org_name: str,
261
+ repo_name: str,
262
+ ) -> None:
263
+ """Test utility for incremental build of cloud repository."""
264
+ check_exponent_version_and_upgrade(settings)
265
+
266
+ if not settings.api_key:
267
+ redirect_to_login(settings)
268
+ return
269
+
270
+ loop = asyncio.get_event_loop()
271
+
272
+ api_key = settings.api_key
273
+ base_api_url = settings.get_base_api_url()
274
+ base_ws_url = settings.get_base_ws_url()
275
+
276
+ try:
277
+ result = loop.run_until_complete(
278
+ incremental_build_cloud_repository(
279
+ api_key, base_api_url, base_ws_url, org_name, repo_name
280
+ )
281
+ )
282
+
283
+ if result["__typename"] == "ContainerImage":
284
+ click.secho(
285
+ f"✓ Successfully triggered incremental build for {org_name}/{repo_name}",
286
+ fg="green",
287
+ )
288
+ click.echo(f" Build ref: {result.get('buildRef', 'N/A')}")
289
+ click.echo(f" Created at: {result.get('createdAt', 'N/A')}")
290
+ click.echo(f" Updated at: {result.get('updatedAt', 'N/A')}")
291
+ else:
292
+ click.secho(
293
+ f"✗ Failed to trigger incremental build: {result.get('message', 'Unknown error')}",
294
+ fg="red",
295
+ )
296
+ click.echo(f" Error type: {result['__typename']}")
297
+
298
+ except Exception as e:
299
+ click.secho(f"✗ Error triggering incremental build: {e!s}", fg="red")
300
+ sys.exit(1)
301
+
302
+
303
+ @cloud_cli.command(hidden=True)
304
+ @click.option(
305
+ "--org-name",
306
+ help="GitHub organization name",
307
+ required=True,
308
+ )
309
+ @click.option(
310
+ "--repo-name",
311
+ help="GitHub repository name",
312
+ required=True,
313
+ )
314
+ @use_settings
315
+ def rebuild(
316
+ settings: Settings,
317
+ org_name: str,
318
+ repo_name: str,
319
+ ) -> None:
320
+ """Test utility for full rebuild of cloud repository."""
321
+ check_exponent_version_and_upgrade(settings)
322
+
323
+ if not settings.api_key:
324
+ redirect_to_login(settings)
325
+ return
326
+
327
+ loop = asyncio.get_event_loop()
328
+
329
+ api_key = settings.api_key
330
+ base_api_url = settings.get_base_api_url()
331
+ base_ws_url = settings.get_base_ws_url()
332
+
333
+ try:
334
+ result = loop.run_until_complete(
335
+ rebuild_cloud_repository(
336
+ api_key, base_api_url, base_ws_url, org_name, repo_name
337
+ )
338
+ )
339
+
340
+ if result["__typename"] == "ContainerImage":
341
+ click.secho(
342
+ f"✓ Successfully triggered rebuild for {org_name}/{repo_name}",
343
+ fg="green",
344
+ )
345
+ click.echo(f" Build ref: {result.get('buildRef', 'N/A')}")
346
+ click.echo(f" Created at: {result.get('createdAt', 'N/A')}")
347
+ click.echo(f" Updated at: {result.get('updatedAt', 'N/A')}")
348
+ else:
349
+ click.secho(
350
+ f"✗ Failed to trigger rebuild: {result.get('message', 'Unknown error')}",
351
+ fg="red",
352
+ )
353
+ click.echo(f" Error type: {result['__typename']}")
354
+
355
+ except Exception as e:
356
+ click.secho(f"✗ Error triggering rebuild: {e!s}", fg="red")
357
+ sys.exit(1)
358
+
359
+
360
+ @cloud_cli.command(hidden=True)
361
+ @use_settings
362
+ def list_repos(
363
+ settings: Settings,
364
+ ) -> None:
365
+ """Test utility for listing GitHub repositories."""
366
+ check_exponent_version_and_upgrade(settings)
367
+
368
+ if not settings.api_key:
369
+ redirect_to_login(settings)
370
+ return
371
+
372
+ loop = asyncio.get_event_loop()
373
+
374
+ api_key = settings.api_key
375
+ base_api_url = settings.get_base_api_url()
376
+ base_ws_url = settings.get_base_ws_url()
377
+
378
+ try:
379
+ result = loop.run_until_complete(
380
+ list_github_repositories(api_key, base_api_url, base_ws_url)
381
+ )
382
+
383
+ if result["__typename"] == "GithubRepositories":
384
+ repositories = result.get("repositories", [])
385
+ if repositories:
386
+ click.secho(f"✓ Found {len(repositories)} repositories:", fg="green")
387
+ for repo in repositories:
388
+ click.echo(
389
+ f"\n Repository: {repo['githubOrgName']}/{repo['githubRepoName']}"
390
+ )
391
+ click.echo(f" ID: {repo['id']}")
392
+ if repo.get("baseHost"):
393
+ click.echo(f" Base Host: {repo['baseHost']}")
394
+ if repo.get("containerImageId"):
395
+ click.echo(
396
+ f" Container Image ID: {repo['containerImageId']}"
397
+ )
398
+ click.echo(f" Created: {repo['createdAt']}")
399
+ click.echo(f" Updated: {repo['updatedAt']}")
400
+ else:
401
+ click.secho("No repositories found", fg="yellow")
402
+ else:
403
+ click.secho(
404
+ f"✗ Failed to list repositories: {result.get('message', 'Unknown error')}",
405
+ fg="red",
406
+ )
407
+ click.echo(f" Error type: {result['__typename']}")
408
+
409
+ except Exception as e:
410
+ click.secho(f"✗ Error listing repositories: {e!s}", fg="red")
411
+ sys.exit(1)
412
+
413
+
414
+ def filter_repositories(
415
+ repositories: list[dict[str, Any]], org_name: str | None, repo_name: str | None
416
+ ) -> list[dict[str, Any]]:
417
+ """Filter repositories by organization and/or repository name."""
418
+ if not (org_name or repo_name):
419
+ return repositories
420
+
421
+ filtered = []
422
+ for repo in repositories:
423
+ if org_name and repo["githubOrgName"] != org_name:
424
+ continue
425
+ if repo_name and repo["githubRepoName"] != repo_name:
426
+ continue
427
+ filtered.append(repo)
428
+
429
+ return filtered
430
+
431
+
432
+ def select_repository_interactive(repositories: list[dict[str, Any]]) -> dict[str, Any]:
433
+ """Interactively select a repository from a list."""
434
+ if len(repositories) == 1:
435
+ selected = repositories[0]
436
+ click.secho(
437
+ f"Using repository: {selected['githubOrgName']}/{selected['githubRepoName']}",
438
+ fg="cyan",
439
+ )
440
+ return selected
441
+
442
+ # Show numbered list for selection
443
+ click.secho("Available repositories:", fg="cyan")
444
+ for i, repo in enumerate(repositories, 1):
445
+ click.echo(f" {i}. {repo['githubOrgName']}/{repo['githubRepoName']}")
446
+
447
+ # Get user selection
448
+ while True:
449
+ try:
450
+ choice = click.prompt("Select a repository (number)", type=int)
451
+ if 1 <= choice <= len(repositories):
452
+ return cast(dict[str, Any], repositories[choice - 1])
453
+ else:
454
+ click.secho(
455
+ f"Please enter a number between 1 and {len(repositories)}",
456
+ fg="red",
457
+ )
458
+ except (ValueError, KeyboardInterrupt):
459
+ click.secho("\nCancelled", fg="yellow")
460
+ sys.exit(0)
461
+
462
+
463
+ def send_initial_prompt(
464
+ loop: asyncio.AbstractEventLoop,
465
+ api_key: str,
466
+ base_api_url: str,
467
+ base_ws_url: str,
468
+ chat_uuid: str,
469
+ prompt: str,
470
+ ) -> None:
471
+ """Send an initial prompt to the chat if provided."""
472
+ click.secho(
473
+ f"\nSending initial prompt: {prompt[:50]}{'...' if len(prompt) > 50 else ''}",
474
+ fg="cyan",
475
+ )
476
+
477
+ prompt_result = loop.run_until_complete(
478
+ start_chat_turn_with_prompt(
479
+ api_key, base_api_url, base_ws_url, chat_uuid, prompt
480
+ )
481
+ )
482
+
483
+ if prompt_result["__typename"] == "Chat":
484
+ click.secho("✓ Prompt sent successfully", fg="green")
485
+ else:
486
+ click.secho(
487
+ f"⚠ Failed to send prompt: {prompt_result.get('message', 'Unknown error')}",
488
+ fg="yellow",
489
+ )
490
+ click.echo(f" Error type: {prompt_result['__typename']}")
491
+
492
+
493
+ def fetch_repositories(
494
+ loop: asyncio.AbstractEventLoop,
495
+ api_key: str,
496
+ base_api_url: str,
497
+ base_ws_url: str,
498
+ ) -> list[dict[str, Any]]:
499
+ """Fetch the list of GitHub repositories."""
500
+ result = loop.run_until_complete(
501
+ list_github_repositories(api_key, base_api_url, base_ws_url)
502
+ )
503
+
504
+ if result["__typename"] != "GithubRepositories":
505
+ click.secho(
506
+ f"✗ Failed to list repositories: {result.get('message', 'Unknown error')}",
507
+ fg="red",
508
+ )
509
+ sys.exit(1)
510
+
511
+ repositories = result.get("repositories", [])
512
+ if not repositories:
513
+ click.secho("No repositories found", fg="yellow")
514
+ sys.exit(1)
515
+
516
+ return cast(list[dict[str, Any]], repositories)
517
+
518
+
519
+ @cloud_cli.command(hidden=True)
520
+ @click.option(
521
+ "--org-name",
522
+ help="GitHub organization name (optional, for filtering)",
523
+ required=False,
524
+ )
525
+ @click.option(
526
+ "--repo-name",
527
+ help="GitHub repository name (optional, for direct selection)",
528
+ required=False,
529
+ )
530
+ @click.option(
531
+ "--prompt",
532
+ help="Initial prompt to send to the chat after creation",
533
+ required=False,
534
+ )
535
+ @use_settings
536
+ def create_chat(
537
+ settings: Settings,
538
+ org_name: str | None,
539
+ repo_name: str | None,
540
+ prompt: str | None,
541
+ ) -> None:
542
+ """Create a cloud chat for a GitHub repository with optional initial prompt."""
543
+ check_exponent_version_and_upgrade(settings)
544
+
545
+ if not settings.api_key:
546
+ redirect_to_login(settings)
547
+ return
548
+
549
+ loop = asyncio.get_event_loop()
550
+ api_key = settings.api_key
551
+ base_url = settings.base_url
552
+ base_api_url = settings.get_base_api_url()
553
+ base_ws_url = settings.get_base_ws_url()
554
+
555
+ try:
556
+ # Fetch repositories
557
+ repositories = fetch_repositories(loop, api_key, base_api_url, base_ws_url)
558
+
559
+ # Filter if criteria provided
560
+ filtered_repos = filter_repositories(repositories, org_name, repo_name)
561
+
562
+ if not filtered_repos:
563
+ click.secho(
564
+ f"No repositories found matching {org_name}/{repo_name or '*'}",
565
+ fg="yellow",
566
+ )
567
+ sys.exit(1)
568
+
569
+ # Select repository
570
+ selected_repo = select_repository_interactive(filtered_repos)
571
+
572
+ # Create cloud chat
573
+ click.secho(
574
+ f"\nCreating cloud chat for {selected_repo['githubOrgName']}/{selected_repo['githubRepoName']}...",
575
+ fg="cyan",
576
+ )
577
+
578
+ chat_result = loop.run_until_complete(
579
+ create_cloud_chat_from_repository(
580
+ api_key, base_api_url, base_ws_url, selected_repo["id"]
581
+ )
582
+ )
583
+
584
+ if chat_result["__typename"] != "Chat":
585
+ click.secho(
586
+ f"✗ Failed to create cloud chat: {chat_result.get('message', 'Unknown error')}",
587
+ fg="red",
588
+ )
589
+ click.echo(f" Error type: {chat_result['__typename']}")
590
+ sys.exit(1)
591
+
592
+ # Success - handle chat creation
593
+ chat_uuid = chat_result["chatUuid"]
594
+ click.secho(f"✓ Successfully created cloud chat: {chat_uuid}", fg="green")
595
+ click.echo(f"\nChat URL: {base_url}/chats/{chat_uuid}")
596
+
597
+ # Send initial prompt if provided
598
+ if prompt:
599
+ send_initial_prompt(
600
+ loop, api_key, base_api_url, base_ws_url, chat_uuid, prompt
601
+ )
602
+
603
+ # Open browser
604
+ launch_exponent_browser(settings.environment, base_url, chat_uuid)
605
+
606
+ except Exception as e:
607
+ click.secho(f"✗ Error creating cloud chat: {e!s}", fg="red")
608
+ sys.exit(1)
609
+
610
+
611
+ @cloud_cli.command(hidden=True)
612
+ @click.option(
613
+ "--cloud-config-id",
614
+ help="ID of an existing cloud config to reconnect",
615
+ required=True,
616
+ )
617
+ @click.option(
618
+ "--prompt",
619
+ help="Prompt to kick off the cloud session.",
620
+ required=True,
621
+ )
622
+ @click.option(
623
+ "--background",
624
+ "-b",
625
+ help="Start the cloud session without launching the Exponent UI",
626
+ is_flag=True,
627
+ default=False,
628
+ )
629
+ @use_settings
630
+ def cloud(
631
+ settings: Settings,
632
+ cloud_config_id: str,
633
+ prompt: str,
634
+ background: bool,
635
+ ) -> None:
636
+ check_exponent_version_and_upgrade(settings)
637
+
638
+ if not settings.api_key:
639
+ redirect_to_login(settings)
640
+ return
641
+
642
+ loop = asyncio.get_event_loop()
643
+
644
+ check_running_from_home_directory()
645
+ loop.run_until_complete(check_inside_git_repo(settings))
646
+ check_ssl()
647
+
648
+ api_key = settings.api_key
649
+ base_url = settings.base_url
650
+ base_api_url = settings.get_base_api_url()
651
+ base_ws_url = settings.get_base_ws_url()
652
+
653
+ chat_uuid = loop.run_until_complete(
654
+ create_cloud_chat(api_key, base_api_url, base_ws_url, cloud_config_id)
655
+ )
656
+
657
+ if chat_uuid is None:
658
+ sys.exit(1)
659
+
660
+ loop.run_until_complete(
661
+ start_chat_turn(api_key, base_api_url, base_ws_url, chat_uuid, prompt)
662
+ )
663
+
664
+ print_exponent_message(base_url, chat_uuid)
665
+
666
+ if not background:
667
+ launch_exponent_browser(settings.environment, base_url, chat_uuid)