indent 0.1.10__py3-none-any.whl → 0.1.12__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 (34) hide show
  1. exponent/__init__.py +16 -3
  2. exponent/commands/cloud_commands.py +582 -0
  3. exponent/commands/common.py +4 -9
  4. exponent/commands/config_commands.py +1 -161
  5. exponent/commands/run_commands.py +20 -9
  6. exponent/commands/utils.py +3 -3
  7. exponent/commands/workflow_commands.py +2 -2
  8. exponent/core/config.py +0 -1
  9. exponent/core/graphql/mutations.py +114 -0
  10. exponent/core/graphql/queries.py +23 -0
  11. exponent/core/graphql/subscriptions.py +0 -449
  12. exponent/core/remote_execution/cli_rpc_types.py +48 -1
  13. exponent/core/remote_execution/client.py +114 -26
  14. exponent/core/remote_execution/file_write.py +1 -376
  15. exponent/core/remote_execution/files.py +1 -102
  16. exponent/core/remote_execution/git.py +1 -1
  17. exponent/core/remote_execution/http_fetch.py +3 -4
  18. exponent/core/remote_execution/languages/python_execution.py +1 -1
  19. exponent/core/remote_execution/languages/shell_streaming.py +1 -1
  20. exponent/core/remote_execution/session.py +1 -1
  21. exponent/core/remote_execution/system_context.py +0 -3
  22. exponent/core/remote_execution/tool_execution.py +24 -4
  23. exponent/core/remote_execution/truncation.py +51 -47
  24. exponent/core/remote_execution/types.py +25 -79
  25. exponent/core/remote_execution/utils.py +23 -51
  26. exponent/core/types/event_types.py +2 -2
  27. exponent/core/types/generated/strategy_info.py +0 -12
  28. exponent/utils/version.py +1 -1
  29. {indent-0.1.10.dist-info → indent-0.1.12.dist-info}/METADATA +3 -3
  30. indent-0.1.12.dist-info/RECORD +52 -0
  31. exponent/core/graphql/cloud_config_queries.py +0 -77
  32. indent-0.1.10.dist-info/RECORD +0 -53
  33. {indent-0.1.10.dist-info → indent-0.1.12.dist-info}/WHEEL +0 -0
  34. {indent-0.1.10.dist-info → indent-0.1.12.dist-info}/entry_points.txt +0 -0
@@ -1,21 +1,13 @@
1
1
  import os
2
- from asyncio import gather, to_thread
2
+ from asyncio import to_thread
3
3
  from typing import Final, cast
4
4
 
5
5
  from anyio import Path as AsyncPath
6
6
  from python_ripgrep import PySortMode, PySortModeKind, files, search
7
- from rapidfuzz import process
8
7
 
9
8
  from exponent.core.remote_execution.cli_rpc_types import ErrorToolResult, GrepToolResult
10
9
  from exponent.core.remote_execution.types import (
11
- FileAttachment,
12
10
  FilePath,
13
- GetFileAttachmentRequest,
14
- GetFileAttachmentResponse,
15
- GetFileAttachmentsRequest,
16
- GetFileAttachmentsResponse,
17
- GetMatchingFilesRequest,
18
- GetMatchingFilesResponse,
19
11
  ListFilesRequest,
20
12
  ListFilesResponse,
21
13
  RemoteFile,
@@ -114,99 +106,6 @@ async def get_file_content(
114
106
  return content, exists
115
107
 
116
108
 
117
- async def get_file_attachments(
118
- get_file_attachments_request: GetFileAttachmentsRequest,
119
- client_working_directory: str,
120
- ) -> GetFileAttachmentsResponse:
121
- """Get the content of the files at the specified paths.
122
-
123
- Args:
124
- get_file_attachments_request: An object containing the file paths.
125
- client_working_directory: The working directory of the client.
126
-
127
- Returns:
128
- A list of FileAttachment objects containing the content of the files.
129
- """
130
- remote_files = get_file_attachments_request.files
131
- attachments = await gather(
132
- *[
133
- get_file_content(
134
- AsyncPath(client_working_directory) / remote_file.file_path
135
- )
136
- for remote_file in remote_files
137
- ]
138
- )
139
-
140
- files = [
141
- FileAttachment(attachment_type="file", file=remote_file, content=content)
142
- for remote_file, (content, _) in zip(remote_files, attachments)
143
- ]
144
-
145
- return GetFileAttachmentsResponse(
146
- correlation_id=get_file_attachments_request.correlation_id,
147
- file_attachments=files,
148
- )
149
-
150
-
151
- async def get_file_attachment(
152
- get_file_attachment_request: GetFileAttachmentRequest, client_working_directory: str
153
- ) -> GetFileAttachmentResponse:
154
- """Get the content of the file at the specified path.
155
-
156
- Args:
157
- get_file_attachment_request: An object containing the file path.
158
- client_working_directory: The working directory of the client.
159
-
160
- Returns:
161
- A FileAttachment object containing the content of the file.
162
- """
163
- file = get_file_attachment_request.file
164
- absolute_path = await file.resolve(client_working_directory)
165
-
166
- content, exists = await get_file_content(absolute_path)
167
-
168
- return GetFileAttachmentResponse(
169
- content=content,
170
- exists=exists,
171
- file=file,
172
- correlation_id=get_file_attachment_request.correlation_id,
173
- )
174
-
175
-
176
- async def get_matching_files(
177
- search_term: GetMatchingFilesRequest,
178
- file_cache: FileCache,
179
- ) -> GetMatchingFilesResponse:
180
- """Get the files that match the search term.
181
-
182
- Args:
183
- search_term: The search term to match against the files.
184
- file_cache: A cache of the files in the working directory.
185
-
186
- Returns:
187
- A list of RemoteFile objects that match the search term.
188
- """
189
- # Use rapidfuzz to find the best matching files
190
- matching_files = await to_thread(
191
- process.extract,
192
- search_term.search_term,
193
- await file_cache.get_files(),
194
- limit=MAX_MATCHING_FILES,
195
- score_cutoff=0,
196
- )
197
-
198
- directory = file_cache.working_directory
199
- files: list[RemoteFile] = [
200
- RemoteFile(file_path=file, working_directory=directory)
201
- for file, _, _ in matching_files
202
- ]
203
-
204
- return GetMatchingFilesResponse(
205
- files=files,
206
- correlation_id=search_term.correlation_id,
207
- )
208
-
209
-
210
109
  async def search_files(
211
110
  path_str: str,
212
111
  file_pattern: str | None,
@@ -167,7 +167,7 @@ async def _get_git_branch(repo: Repository) -> str | None:
167
167
  else:
168
168
  return None
169
169
 
170
- except Exception: # noqa: BLE001
170
+ except Exception:
171
171
  return None
172
172
 
173
173
 
@@ -1,13 +1,12 @@
1
1
  """HTTP fetch implementation for remote execution client."""
2
2
 
3
3
  import logging
4
- from typing import Any
5
4
 
6
5
  import httpx
7
6
 
8
7
  from exponent.core.remote_execution.cli_rpc_types import (
9
- HttpResponse,
10
8
  HttpRequest,
9
+ HttpResponse,
11
10
  )
12
11
 
13
12
  logger = logging.getLogger(__name__)
@@ -79,7 +78,7 @@ async def fetch_http_content(http_request: HttpRequest) -> HttpResponse:
79
78
  )
80
79
 
81
80
  except httpx.RequestError as e:
82
- error_msg = f"Request error for {http_request.url}: {str(e)}"
81
+ error_msg = f"Request error for {http_request.url}: {e!s}"
83
82
  return HttpResponse(
84
83
  status_code=None,
85
84
  content="",
@@ -87,7 +86,7 @@ async def fetch_http_content(http_request: HttpRequest) -> HttpResponse:
87
86
  )
88
87
 
89
88
  except Exception as e:
90
- error_msg = f"Unexpected error fetching {http_request.url}: {str(e)}"
89
+ error_msg = f"Unexpected error fetching {http_request.url}: {e!s}"
91
90
  return HttpResponse(
92
91
  status_code=None,
93
92
  content="",
@@ -140,7 +140,7 @@ class Kernel:
140
140
  except queue.Empty:
141
141
  continue
142
142
 
143
- except Exception as e: # noqa: BLE001 - TODO: Deep audit potential exceptions
143
+ except Exception as e:
144
144
  logger.info(f"Error getting message from kernel: {e}")
145
145
  break
146
146
 
@@ -75,7 +75,7 @@ async def read_stream(
75
75
  break
76
76
 
77
77
 
78
- async def execute_shell_streaming(
78
+ async def execute_shell_streaming( # noqa: PLR0915
79
79
  code: str,
80
80
  working_directory: str,
81
81
  timeout: int,
@@ -120,7 +120,7 @@ async def get_session(
120
120
  )
121
121
  try:
122
122
  yield session
123
- except Exception as exc: # noqa: BLE001
123
+ except Exception as exc:
124
124
  await send_exception_log(exc, session=session, settings=None)
125
125
  raise ExponentError(str(exc))
126
126
  finally:
@@ -2,14 +2,11 @@ import getpass
2
2
  import os
3
3
  import platform
4
4
 
5
- from anyio import Path as AsyncPath
6
-
7
5
  from exponent.core.remote_execution.git import get_git_info
8
6
  from exponent.core.remote_execution.languages import python_execution
9
7
  from exponent.core.remote_execution.types import (
10
8
  SystemInfo,
11
9
  )
12
- from exponent.core.remote_execution.utils import safe_read_file
13
10
 
14
11
 
15
12
  async def get_system_info(working_directory: str) -> SystemInfo:
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import uuid
2
3
  from collections.abc import Callable
3
4
  from pathlib import Path
@@ -36,9 +37,12 @@ from exponent.core.remote_execution.types import (
36
37
  )
37
38
  from exponent.core.remote_execution.utils import (
38
39
  assert_unreachable,
40
+ safe_get_file_metadata,
39
41
  safe_read_file,
40
42
  )
41
43
 
44
+ logger = logging.getLogger(__name__)
45
+
42
46
 
43
47
  async def execute_tool(
44
48
  tool_input: ToolInputType, working_directory: str
@@ -116,9 +120,11 @@ async def execute_read_file( # noqa: PLR0911
116
120
  return ErrorToolResult(
117
121
  error_message="File appears to be binary or has invalid text encoding"
118
122
  )
119
- except Exception as e: # noqa: BLE001
123
+ except Exception as e:
120
124
  return ErrorToolResult(error_message=f"Error reading file: {e!s}")
121
125
 
126
+ metadata = await safe_get_file_metadata(file)
127
+
122
128
  # Handle empty files
123
129
  if not content:
124
130
  return ReadToolResult(
@@ -126,6 +132,7 @@ async def execute_read_file( # noqa: PLR0911
126
132
  num_lines=0,
127
133
  start_line=0,
128
134
  total_lines=0,
135
+ metadata=metadata,
129
136
  )
130
137
 
131
138
  content_lines = content.splitlines(keepends=True)
@@ -138,6 +145,7 @@ async def execute_read_file( # noqa: PLR0911
138
145
  num_lines=0,
139
146
  start_line=offset,
140
147
  total_lines=total_lines,
148
+ metadata=metadata,
141
149
  )
142
150
 
143
151
  # Apply offset and limit
@@ -180,6 +188,7 @@ async def execute_read_file( # noqa: PLR0911
180
188
  num_lines=num_lines,
181
189
  start_line=offset,
182
190
  total_lines=total_lines,
191
+ metadata=metadata,
183
192
  )
184
193
 
185
194
 
@@ -213,6 +222,16 @@ async def execute_edit_file( # noqa: PLR0911
213
222
  if not exists:
214
223
  return ErrorToolResult(error_message="File not found")
215
224
 
225
+ if tool_input.last_known_modified_timestamp is not None:
226
+ metadata = await safe_get_file_metadata(file)
227
+ if (
228
+ metadata is not None
229
+ and metadata.modified_timestamp > tool_input.last_known_modified_timestamp
230
+ ):
231
+ return ErrorToolResult(
232
+ error_message="File has been modified since last read/write"
233
+ )
234
+
216
235
  try:
217
236
  if await file.is_dir():
218
237
  return ErrorToolResult(
@@ -232,7 +251,7 @@ async def execute_edit_file( # noqa: PLR0911
232
251
  return ErrorToolResult(
233
252
  error_message="File appears to be binary or has invalid text encoding"
234
253
  )
235
- except Exception as e: # noqa: BLE001
254
+ except Exception as e:
236
255
  return ErrorToolResult(error_message=f"Error reading file: {e!s}")
237
256
 
238
257
  # Check if search text exists
@@ -265,9 +284,10 @@ async def execute_edit_file( # noqa: PLR0911
265
284
  path = Path(working_directory, tool_input.file_path)
266
285
  await execute_full_file_rewrite(path, new_content, working_directory)
267
286
  return EditToolResult(
268
- message=f"Successfully replaced text in {tool_input.file_path}"
287
+ message=f"Successfully replaced text in {tool_input.file_path}",
288
+ metadata=await safe_get_file_metadata(path),
269
289
  )
270
- except Exception as e: # noqa: BLE001
290
+ except Exception as e:
271
291
  return ErrorToolResult(error_message=f"Error writing file: {e!s}")
272
292
 
273
293
 
@@ -123,6 +123,56 @@ class CompositeTruncation(TruncationStrategy):
123
123
  return result
124
124
 
125
125
 
126
+ class TailTruncation(TruncationStrategy):
127
+ """Truncation strategy that keeps the end of the output (tail) instead of the beginning."""
128
+
129
+ def __init__(
130
+ self,
131
+ field_name: str,
132
+ character_limit: int = DEFAULT_CHARACTER_LIMIT,
133
+ ):
134
+ self.field_name = field_name
135
+ self.character_limit = character_limit
136
+
137
+ def should_truncate(self, result: ToolResult) -> bool:
138
+ if hasattr(result, self.field_name):
139
+ value = getattr(result, self.field_name)
140
+ if isinstance(value, str):
141
+ return len(value) > self.character_limit
142
+ return False
143
+
144
+ def truncate(self, result: ToolResult) -> ToolResult:
145
+ if not hasattr(result, self.field_name):
146
+ return result
147
+
148
+ value = getattr(result, self.field_name)
149
+ if not isinstance(value, str):
150
+ return result
151
+
152
+ if len(value) <= self.character_limit:
153
+ return result
154
+
155
+ # Keep the last character_limit characters
156
+ truncated_value = value[-self.character_limit :]
157
+
158
+ # Try to start at a newline if possible for cleaner output
159
+ newline_pos = truncated_value.find("\n")
160
+ if (
161
+ newline_pos != -1 and newline_pos < 1000
162
+ ): # Only adjust if newline is reasonably close to start
163
+ truncated_value = truncated_value[newline_pos + 1 :]
164
+
165
+ # Add truncation indicator at the beginning
166
+ truncation_msg = f"... (output truncated, showing last {len(truncated_value)} characters) ...\n"
167
+ truncated_value = truncation_msg + truncated_value
168
+
169
+ updates: dict[str, Any] = {self.field_name: truncated_value}
170
+ if hasattr(result, "truncated"):
171
+ updates["truncated"] = True
172
+
173
+ return replace(result, **updates)
174
+
175
+
126
176
  class StringListTruncation(TruncationStrategy):
127
177
  """Truncation for lists of strings that limits both number of items and individual string length."""
128
178
 
@@ -215,7 +265,7 @@ class StringListTruncation(TruncationStrategy):
215
265
  TRUNCATION_REGISTRY: dict[type[ToolResult], TruncationStrategy] = {
216
266
  ReadToolResult: StringFieldTruncation("content"),
217
267
  WriteToolResult: StringFieldTruncation("message"),
218
- BashToolResult: StringFieldTruncation("shell_output"),
268
+ BashToolResult: TailTruncation("shell_output"),
219
269
  GrepToolResult: StringListTruncation("matches"),
220
270
  GlobToolResult: StringListTruncation("filenames", max_item_length=4096),
221
271
  ListToolResult: StringListTruncation("files", max_item_length=4096),
@@ -236,49 +286,3 @@ def truncate_tool_result(result: T) -> T:
236
286
  return cast(T, strategy.truncate(result))
237
287
 
238
288
  return result
239
-
240
-
241
- def register_truncation_strategy(
242
- result_type: type[ToolResult],
243
- strategy: TruncationStrategy,
244
- ) -> None:
245
- TRUNCATION_REGISTRY[result_type] = strategy
246
-
247
-
248
- def configure_truncation_limits(
249
- character_limit: int | None = None,
250
- list_item_limit: int | None = None,
251
- list_preview_items: int | None = None,
252
- string_item_length: int | None = None,
253
- ) -> None:
254
- for strategy in TRUNCATION_REGISTRY.values():
255
- if isinstance(strategy, StringFieldTruncation) and character_limit is not None:
256
- strategy.character_limit = character_limit
257
- elif isinstance(strategy, ListFieldTruncation):
258
- if list_item_limit is not None:
259
- strategy.item_limit = list_item_limit
260
- if list_preview_items is not None:
261
- strategy.preview_items = list_preview_items
262
- elif isinstance(strategy, StringListTruncation):
263
- if list_item_limit is not None:
264
- strategy.max_items = list_item_limit
265
- if list_preview_items is not None:
266
- strategy.preview_items = list_preview_items
267
- if string_item_length is not None:
268
- strategy.max_item_length = string_item_length
269
- elif isinstance(strategy, CompositeTruncation):
270
- for sub_strategy in strategy.strategies:
271
- if isinstance(sub_strategy, StringFieldTruncation) and character_limit:
272
- sub_strategy.character_limit = character_limit
273
- elif isinstance(sub_strategy, ListFieldTruncation):
274
- if list_item_limit is not None:
275
- sub_strategy.item_limit = list_item_limit
276
- if list_preview_items is not None:
277
- sub_strategy.preview_items = list_preview_items
278
- elif isinstance(sub_strategy, StringListTruncation):
279
- if list_item_limit is not None:
280
- sub_strategy.max_items = list_item_limit
281
- if list_preview_items is not None:
282
- sub_strategy.preview_items = list_preview_items
283
- if string_item_length is not None:
284
- sub_strategy.max_item_length = string_item_length
@@ -11,7 +11,6 @@ from typing import (
11
11
  Generic,
12
12
  Literal,
13
13
  TypeVar,
14
- Union,
15
14
  )
16
15
 
17
16
  from anyio import Path as AsyncPath
@@ -99,6 +98,7 @@ class HeartbeatInfo(BaseModel):
99
98
  default_factory=lambda: datetime.datetime.now(datetime.UTC)
100
99
  )
101
100
  timestamp_received: datetime.datetime | None = None
101
+ cli_uuid: str | None = None
102
102
 
103
103
 
104
104
  class RemoteFile(BaseModel):
@@ -226,9 +226,6 @@ Namespace = Literal[
226
226
  "file_write",
227
227
  "command",
228
228
  "list_files",
229
- "get_file_attachment",
230
- "get_file_attachments",
231
- "get_matching_files",
232
229
  "error",
233
230
  "create_checkpoint",
234
231
  "rollback_to_checkpoint",
@@ -333,24 +330,6 @@ class ListFilesResponse(RemoteExecutionResponse):
333
330
  files: list[RemoteFile]
334
331
 
335
332
 
336
- class GetFileAttachmentResponse(RemoteExecutionResponse, FileAttachment):
337
- namespace: ClassVar[Namespace] = "get_file_attachment"
338
-
339
- exists: bool = Field(default=True)
340
-
341
-
342
- class GetFileAttachmentsResponse(RemoteExecutionResponse):
343
- namespace: ClassVar[Namespace] = "get_file_attachments"
344
-
345
- file_attachments: list[FileAttachment]
346
-
347
-
348
- class GetMatchingFilesResponse(RemoteExecutionResponse):
349
- namespace: ClassVar[Namespace] = "get_matching_files"
350
-
351
- files: list[RemoteFile]
352
-
353
-
354
333
  class ErrorResponse(RemoteExecutionResponse):
355
334
  namespace: ClassVar[Namespace] = "error"
356
335
  # The namespace of the request that caused the error.
@@ -425,7 +404,7 @@ class CodeExecutionRequest(RemoteExecutionRequest[CodeExecutionResponse]):
425
404
 
426
405
  class StreamingCodeExecutionRequest(
427
406
  RemoteExecutionRequest[
428
- Union[StreamingCodeExecutionResponseChunk, StreamingCodeExecutionResponse]
407
+ StreamingCodeExecutionResponseChunk | StreamingCodeExecutionResponse
429
408
  ]
430
409
  ):
431
410
  namespace: ClassVar[Namespace] = "streaming_code_execution"
@@ -452,24 +431,6 @@ class ListFilesRequest(RemoteExecutionRequest[ListFilesResponse]):
452
431
  directory: str
453
432
 
454
433
 
455
- class GetFileAttachmentRequest(RemoteExecutionRequest[GetFileAttachmentResponse]):
456
- namespace: ClassVar[Namespace] = "get_file_attachment"
457
-
458
- file: RemoteFile
459
-
460
-
461
- class GetFileAttachmentsRequest(RemoteExecutionRequest[GetFileAttachmentsResponse]):
462
- namespace: ClassVar[Namespace] = "get_file_attachments"
463
-
464
- files: list[RemoteFile]
465
-
466
-
467
- class GetMatchingFilesRequest(RemoteExecutionRequest[GetMatchingFilesResponse]):
468
- namespace: ClassVar[Namespace] = "get_matching_files"
469
-
470
- search_term: str
471
-
472
-
473
434
  class CreateCheckpointRequest(RemoteExecutionRequest[CreateCheckpointResponse]):
474
435
  namespace: ClassVar[Namespace] = "create_checkpoint"
475
436
 
@@ -508,39 +469,31 @@ class CommandRequest(RemoteExecutionRequest[CommandResponse]):
508
469
  data: CommandDataType = Field(..., discriminator="type")
509
470
 
510
471
 
511
- RemoteExecutionRequestType = Union[
512
- CodeExecutionRequest,
513
- FileWriteRequest,
514
- ListFilesRequest,
515
- GetFileAttachmentRequest,
516
- GetFileAttachmentsRequest,
517
- GetMatchingFilesRequest,
518
- CommandRequest,
519
- StreamingCodeExecutionRequest,
520
- CreateCheckpointRequest,
521
- RollbackToCheckpointRequest,
522
- ]
472
+ RemoteExecutionRequestType = (
473
+ CodeExecutionRequest
474
+ | FileWriteRequest
475
+ | ListFilesRequest
476
+ | CommandRequest
477
+ | StreamingCodeExecutionRequest
478
+ | CreateCheckpointRequest
479
+ | RollbackToCheckpointRequest
480
+ )
523
481
 
524
- RemoteExecutionResponseType = Union[
525
- CodeExecutionResponse,
526
- StreamingCodeExecutionResponseChunk,
527
- StreamingCodeExecutionResponse,
528
- FileWriteResponse,
529
- ListFilesResponse,
530
- GetFileAttachmentResponse,
531
- GetFileAttachmentsResponse,
532
- GetMatchingFilesResponse,
533
- CommandResponse,
534
- ErrorResponse,
535
- CreateCheckpointResponse,
536
- RollbackToCheckpointResponse,
537
- ]
482
+ RemoteExecutionResponseType = (
483
+ CodeExecutionResponse
484
+ | StreamingCodeExecutionResponseChunk
485
+ | StreamingCodeExecutionResponse
486
+ | FileWriteResponse
487
+ | ListFilesResponse
488
+ | CommandResponse
489
+ | ErrorResponse
490
+ | CreateCheckpointResponse
491
+ | RollbackToCheckpointResponse
492
+ )
538
493
 
539
- StreamingResponseType = Union[
540
- StreamingCodeExecutionResponseChunk,
541
- StreamingCodeExecutionResponse,
542
- ErrorResponse,
543
- ]
494
+ StreamingResponseType = (
495
+ StreamingCodeExecutionResponseChunk | StreamingCodeExecutionResponse | ErrorResponse
496
+ )
544
497
 
545
498
  STREAMING_NAMESPACES = [
546
499
  "streaming_code_execution",
@@ -553,17 +506,10 @@ class ChatMode(str, Enum):
553
506
  CLI = "CLI"
554
507
  CLOUD = "CLOUD" # chat with cloud devbox
555
508
  CODEBASE = "CODEBASE" # chat with codebase
556
- PYTHON_INTERPRETER = "PYTHON_INTERPRETER"
557
509
  DATABASE = "DATABASE" # chat with database connection
558
510
  WORKFLOW = "WORKFLOW"
559
511
 
560
512
 
561
- DEVBOX_CHAT_MODES = [
562
- ChatMode.CLOUD,
563
- ChatMode.CODEBASE,
564
- ]
565
-
566
-
567
513
  class ChatSource(str, Enum):
568
514
  CLI_SHELL = "CLI_SHELL"
569
515
  CLI_RUN = "CLI_RUN"