indent 0.1.26__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.
Files changed (55) hide show
  1. exponent/__init__.py +34 -0
  2. exponent/cli.py +110 -0
  3. exponent/commands/cloud_commands.py +585 -0
  4. exponent/commands/common.py +411 -0
  5. exponent/commands/config_commands.py +334 -0
  6. exponent/commands/run_commands.py +222 -0
  7. exponent/commands/settings.py +56 -0
  8. exponent/commands/types.py +111 -0
  9. exponent/commands/upgrade.py +29 -0
  10. exponent/commands/utils.py +146 -0
  11. exponent/core/config.py +180 -0
  12. exponent/core/graphql/__init__.py +0 -0
  13. exponent/core/graphql/client.py +61 -0
  14. exponent/core/graphql/get_chats_query.py +47 -0
  15. exponent/core/graphql/mutations.py +160 -0
  16. exponent/core/graphql/queries.py +146 -0
  17. exponent/core/graphql/subscriptions.py +16 -0
  18. exponent/core/remote_execution/checkpoints.py +212 -0
  19. exponent/core/remote_execution/cli_rpc_types.py +499 -0
  20. exponent/core/remote_execution/client.py +999 -0
  21. exponent/core/remote_execution/code_execution.py +77 -0
  22. exponent/core/remote_execution/default_env.py +31 -0
  23. exponent/core/remote_execution/error_info.py +45 -0
  24. exponent/core/remote_execution/exceptions.py +10 -0
  25. exponent/core/remote_execution/file_write.py +35 -0
  26. exponent/core/remote_execution/files.py +330 -0
  27. exponent/core/remote_execution/git.py +268 -0
  28. exponent/core/remote_execution/http_fetch.py +94 -0
  29. exponent/core/remote_execution/languages/python_execution.py +239 -0
  30. exponent/core/remote_execution/languages/shell_streaming.py +226 -0
  31. exponent/core/remote_execution/languages/types.py +20 -0
  32. exponent/core/remote_execution/port_utils.py +73 -0
  33. exponent/core/remote_execution/session.py +128 -0
  34. exponent/core/remote_execution/system_context.py +26 -0
  35. exponent/core/remote_execution/terminal_session.py +375 -0
  36. exponent/core/remote_execution/terminal_types.py +29 -0
  37. exponent/core/remote_execution/tool_execution.py +595 -0
  38. exponent/core/remote_execution/tool_type_utils.py +39 -0
  39. exponent/core/remote_execution/truncation.py +296 -0
  40. exponent/core/remote_execution/types.py +635 -0
  41. exponent/core/remote_execution/utils.py +477 -0
  42. exponent/core/types/__init__.py +0 -0
  43. exponent/core/types/command_data.py +206 -0
  44. exponent/core/types/event_types.py +89 -0
  45. exponent/core/types/generated/__init__.py +0 -0
  46. exponent/core/types/generated/strategy_info.py +213 -0
  47. exponent/migration-docs/login.md +112 -0
  48. exponent/py.typed +4 -0
  49. exponent/utils/__init__.py +0 -0
  50. exponent/utils/colors.py +92 -0
  51. exponent/utils/version.py +289 -0
  52. indent-0.1.26.dist-info/METADATA +38 -0
  53. indent-0.1.26.dist-info/RECORD +55 -0
  54. indent-0.1.26.dist-info/WHEEL +4 -0
  55. indent-0.1.26.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,635 @@
1
+ import datetime
2
+ import json
3
+ from enum import Enum
4
+ from functools import cached_property
5
+ from os import PathLike
6
+ from pathlib import Path, PurePath
7
+ from typing import (
8
+ Annotated,
9
+ Any,
10
+ ClassVar,
11
+ Generic,
12
+ Literal,
13
+ TypeVar,
14
+ )
15
+
16
+ from anyio import Path as AsyncPath
17
+ from pydantic import BaseModel, Field
18
+
19
+ from exponent.core.remote_execution.error_info import SerializableErrorInfo
20
+ from exponent.core.types.command_data import (
21
+ CommandDataType,
22
+ FileWriteStrategyName,
23
+ )
24
+
25
+ type FilePath = str | PathLike[str]
26
+
27
+
28
+ # DEPRECATED, only around for gql compatibility
29
+ class UseToolsMode(str, Enum):
30
+ read_only = "read_only"
31
+ read_write = "read_write"
32
+ disabled = "disabled"
33
+
34
+
35
+ class CreateChatResponse(BaseModel):
36
+ chat_uuid: str
37
+
38
+
39
+ class RunWorkflowRequest(BaseModel):
40
+ chat_uuid: str
41
+ workflow_id: str
42
+
43
+
44
+ # note: before adding fields here, probably update
45
+ # get_workflow_run_by_trigger db query
46
+ class PrReviewWorkflowInput(BaseModel):
47
+ repo_owner: str
48
+ repo_name: str
49
+ pr_number: int
50
+
51
+
52
+ class SlackWorkflowInput(BaseModel):
53
+ discriminator: Literal["slack_workflow"] = "slack_workflow"
54
+ channel_id: str
55
+ thread_ts: str
56
+ slack_url: str | None = None
57
+ channel_name: str | None = None
58
+ message_ts: str | None = None
59
+
60
+
61
+ class SlackPlanApprovalWorkflowInput(BaseModel):
62
+ discriminator: Literal["slack_plan_approval"] = "slack_plan_approval"
63
+ channel_id: str
64
+ thread_ts: str
65
+ slack_url: str
66
+ channel_name: str
67
+ message_ts: str
68
+
69
+
70
+ class SentryWorkflowInput(BaseModel):
71
+ title: str
72
+ issue_id: str
73
+ permalink: str
74
+
75
+
76
+ class GenericCloudWorkflowInput(BaseModel):
77
+ initial_prompt: str
78
+ system_prompt_override: str | None = None
79
+ reasoning_level: str = "LOW"
80
+
81
+
82
+ WorkflowInput = (
83
+ PrReviewWorkflowInput
84
+ | SlackWorkflowInput
85
+ | SentryWorkflowInput
86
+ | GenericCloudWorkflowInput
87
+ | SlackPlanApprovalWorkflowInput
88
+ )
89
+
90
+
91
+ class WorkflowTriggerRequest(BaseModel):
92
+ workflow_name: str
93
+ workflow_input: WorkflowInput
94
+
95
+
96
+ class WorkflowTriggerResponse(BaseModel):
97
+ chat_uuid: str
98
+
99
+
100
+ class ExecutionEndResponse(BaseModel):
101
+ execution_ended: bool
102
+
103
+
104
+ class SignalType(str, Enum):
105
+ disconnect = "disconnect"
106
+
107
+ def __str__(self) -> str:
108
+ return self.value
109
+
110
+
111
+ class GitInfo(BaseModel):
112
+ branch: str
113
+ remote: str | None
114
+
115
+
116
+ class PythonEnvInfo(BaseModel):
117
+ interpreter_path: str | None
118
+ interpreter_version: str | None
119
+ name: str | None = "exponent"
120
+ provider: Literal["venv", "pyenv", "pipenv", "conda"] | None = "pyenv"
121
+
122
+
123
+ class PortInfo(BaseModel):
124
+ process_name: str
125
+ port: int
126
+ protocol: str
127
+ pid: int | None
128
+ uptime_seconds: float | None
129
+
130
+
131
+ class SystemInfo(BaseModel):
132
+ name: str
133
+ cwd: str
134
+ os: str
135
+ shell: str
136
+ git: GitInfo | None
137
+ python_env: PythonEnvInfo | None
138
+ port_usage: list[PortInfo] | None = None
139
+
140
+
141
+ class HeartbeatInfo(BaseModel):
142
+ exponent_version: str | None = None
143
+ editable_installation: bool = False
144
+ system_info: SystemInfo | None
145
+ timestamp: datetime.datetime = Field(
146
+ default_factory=lambda: datetime.datetime.now(datetime.UTC)
147
+ )
148
+ timestamp_received: datetime.datetime | None = None
149
+ cli_uuid: str | None = None
150
+
151
+
152
+ class RemoteFile(BaseModel):
153
+ file_path: str
154
+ working_directory: str = "."
155
+
156
+ @cached_property
157
+ def pure_path(self) -> PurePath:
158
+ return PurePath(self.working_directory, self.file_path)
159
+
160
+ @cached_property
161
+ def path(self) -> Path:
162
+ return Path(self.working_directory, self.file_path)
163
+
164
+ @cached_property
165
+ def name(self) -> str:
166
+ return self.pure_path.name
167
+
168
+ @cached_property
169
+ def absolute_path(self) -> str:
170
+ return self.path.absolute().as_posix()
171
+
172
+ async def resolve(self, client_working_directory: str) -> str:
173
+ working_directory = AsyncPath(self.working_directory, self.file_path)
174
+
175
+ if not working_directory.is_absolute():
176
+ working_directory = AsyncPath(client_working_directory, working_directory)
177
+
178
+ return str(await working_directory.resolve())
179
+
180
+ def __eq__(self, other: object) -> bool:
181
+ if not isinstance(other, RemoteFile):
182
+ return False
183
+
184
+ return self.path.name == other.path.name
185
+
186
+ def __lt__(self, other: "RemoteFile") -> bool:
187
+ # Prefer shorter paths
188
+ if (cmp := self._cmp_path_len(other)) is not None:
189
+ return cmp
190
+
191
+ # Prefer paths sorted by parent directory
192
+ if (cmp := self._cmp_path_str(other)) is not None:
193
+ return cmp
194
+
195
+ # Prefer paths with alphabetical first character
196
+ return self._cmp_first_char(other)
197
+
198
+ def __hash__(self) -> int:
199
+ return hash(self.absolute_path)
200
+
201
+ def _cmp_first_char(self, other: "RemoteFile") -> bool:
202
+ return self._cmp_str(self.path.name, other.path.name)
203
+
204
+ def _cmp_path_len(self, other: "RemoteFile") -> bool | None:
205
+ self_parts = self.path.absolute().parent.parts
206
+ other_parts = other.path.absolute().parent.parts
207
+
208
+ if len(self_parts) == len(other_parts):
209
+ return None
210
+
211
+ return len(self_parts) < len(other_parts)
212
+
213
+ def _cmp_path_str(self, other: "RemoteFile") -> bool | None:
214
+ self_parts = self.path.absolute().parent.parts
215
+ other_parts = other.path.absolute().parent.parts
216
+
217
+ if self_parts == other_parts:
218
+ return None
219
+
220
+ for a, b in zip(self_parts, other_parts):
221
+ if a != b:
222
+ return self._cmp_str(a, b)
223
+
224
+ return False
225
+
226
+ @staticmethod
227
+ def _cmp_str(s1: str, s2: str) -> bool:
228
+ if s1[:1].isalpha() == s2[:1].isalpha():
229
+ return s1 < s2
230
+
231
+ return s1[:1].isalpha()
232
+
233
+
234
+ class URLAttachment(BaseModel):
235
+ attachment_type: Literal["url"] = "url"
236
+ url: str
237
+ content: str
238
+
239
+
240
+ class FileAttachment(BaseModel):
241
+ attachment_type: Literal["file"] = "file"
242
+ file: RemoteFile
243
+ content: str
244
+ truncated: bool = False
245
+
246
+
247
+ class TableSchemaAttachment(BaseModel):
248
+ attachment_type: Literal["table_schema"] = "table_schema"
249
+ table_name: str
250
+ table_schema: dict[str, Any]
251
+
252
+
253
+ class PromptAttachment(BaseModel):
254
+ attachment_type: Literal["prompt"] = "prompt"
255
+ prompt_name: str
256
+ prompt_content: str
257
+
258
+
259
+ class SQLAttachment(BaseModel):
260
+ attachment_type: Literal["sql"] = "sql"
261
+ query_content: str
262
+ query_id: str
263
+
264
+
265
+ MessageAttachment = Annotated[
266
+ FileAttachment
267
+ | URLAttachment
268
+ | TableSchemaAttachment
269
+ | PromptAttachment
270
+ | SQLAttachment,
271
+ Field(discriminator="attachment_type"),
272
+ ]
273
+
274
+
275
+ Direction = Literal[
276
+ "request",
277
+ "response",
278
+ ]
279
+
280
+ Namespace = Literal[
281
+ "code_execution",
282
+ "streaming_code_execution",
283
+ "streaming_code_execution_chunk",
284
+ "file_write",
285
+ "command",
286
+ "list_files",
287
+ "error",
288
+ "create_checkpoint",
289
+ "rollback_to_checkpoint",
290
+ ]
291
+
292
+ ErrorType = Literal["unknown_request_type", "request_error"]
293
+
294
+ SupportedLanguage = Literal[
295
+ "python",
296
+ "shell",
297
+ ]
298
+
299
+ SUPPORTED_LANGUAGES: list[SupportedLanguage] = ["python", "shell"]
300
+
301
+
302
+ class RemoteExecutionMessageData(BaseModel):
303
+ namespace: Namespace
304
+ direction: Direction
305
+ message_data: str
306
+
307
+ def message_type(self) -> str:
308
+ return f"{self.namespace}.{self.direction}"
309
+
310
+
311
+ class RemoteExecutionMessage(BaseModel):
312
+ direction: ClassVar[Direction]
313
+ namespace: ClassVar[Namespace]
314
+ correlation_id: str
315
+
316
+ @classmethod
317
+ def message_type(cls) -> str:
318
+ return f"{cls.namespace}.{cls.direction}"
319
+
320
+ @property
321
+ def result_key(self) -> str:
322
+ return f"{self.namespace}:{self.correlation_id}"
323
+
324
+
325
+ ### Response Types
326
+
327
+
328
+ class RemoteExecutionResponseData(RemoteExecutionMessageData):
329
+ pass
330
+
331
+
332
+ class RemoteExecutionResponse(RemoteExecutionMessage):
333
+ direction: ClassVar[Direction] = "response"
334
+
335
+
336
+ ResponseT = TypeVar("ResponseT", bound=RemoteExecutionResponse)
337
+
338
+
339
+ class StreamingCodeExecutionResponseChunk(RemoteExecutionResponse):
340
+ namespace: ClassVar[Namespace] = "streaming_code_execution_chunk"
341
+
342
+ content: str
343
+ truncated: bool = False
344
+
345
+ def add(
346
+ self, new_chunk: "StreamingCodeExecutionResponseChunk"
347
+ ) -> "StreamingCodeExecutionResponseChunk":
348
+ """Aggregates content of this and a new chunk."""
349
+ assert self.correlation_id == new_chunk.correlation_id
350
+ return StreamingCodeExecutionResponseChunk(
351
+ correlation_id=self.correlation_id, content=self.content + new_chunk.content
352
+ )
353
+
354
+
355
+ class StreamingCodeExecutionResponse(RemoteExecutionResponse):
356
+ namespace: ClassVar[Namespace] = "streaming_code_execution"
357
+
358
+ content: str
359
+ truncated: bool = False
360
+
361
+ # Only present for shell code execution
362
+ cancelled_for_timeout: bool = False
363
+ exit_code: int | None = None
364
+ halted: bool = False
365
+
366
+
367
+ class CodeExecutionResponse(RemoteExecutionResponse):
368
+ namespace: ClassVar[Namespace] = "code_execution"
369
+
370
+ content: str
371
+
372
+ # Only present for shell code execution
373
+ cancelled_for_timeout: bool = False
374
+ exit_code: int | None = None
375
+ halted: bool = False
376
+ truncated: bool = False
377
+
378
+
379
+ class FileWriteResponse(RemoteExecutionResponse):
380
+ namespace: ClassVar[Namespace] = "file_write"
381
+
382
+ content: str
383
+
384
+
385
+ class ListFilesResponse(RemoteExecutionResponse):
386
+ namespace: ClassVar[Namespace] = "list_files"
387
+
388
+ files: list[RemoteFile]
389
+
390
+
391
+ class ErrorResponse(RemoteExecutionResponse):
392
+ namespace: ClassVar[Namespace] = "error"
393
+ # The namespace of the request that caused the error.
394
+ # Not a Namespace to avoid deserialization errors
395
+ request_namespace: str
396
+ error_type: ErrorType
397
+ error_info: SerializableErrorInfo | None = None
398
+
399
+ @property
400
+ def result_key(self) -> str:
401
+ # Match the key of the request that caused the error
402
+ return f"{self.request_namespace}:{self.correlation_id}"
403
+
404
+
405
+ class GitFileChange(BaseModel):
406
+ path: str
407
+ lines_added: int
408
+ lines_deleted: int
409
+
410
+
411
+ class GitDiff(BaseModel):
412
+ files: list[GitFileChange]
413
+ truncated: bool = False # True if there were more files than the limit
414
+ total_files: int # Total number of files changed, even if truncated
415
+
416
+
417
+ class GitCommitMetadata(BaseModel):
418
+ author_name: str
419
+ author_email: str
420
+ author_date: str
421
+ commit_date: str
422
+ commit_message: str
423
+ branch: str
424
+
425
+
426
+ class CreateCheckpointResponse(RemoteExecutionResponse):
427
+ namespace: ClassVar[Namespace] = "create_checkpoint"
428
+
429
+ correlation_id: str
430
+ head_commit_hash: str
431
+ head_commit_metadata: GitCommitMetadata
432
+ uncommitted_changes_commit_hash: str | None = None
433
+ diff_versus_last_checkpoint: GitDiff | None = None
434
+
435
+ debug_info: dict[str, Any] | None = None
436
+
437
+
438
+ class RollbackToCheckpointResponse(RemoteExecutionResponse):
439
+ namespace: ClassVar[Namespace] = "rollback_to_checkpoint"
440
+
441
+ debug_info: dict[str, Any] | None = None
442
+
443
+
444
+ ### Request Types
445
+
446
+
447
+ class RemoteExecutionRequestData(RemoteExecutionMessageData):
448
+ pass
449
+
450
+
451
+ class RemoteExecutionRequest(RemoteExecutionMessage, Generic[ResponseT]):
452
+ direction: ClassVar[Direction] = "request"
453
+
454
+
455
+ class CodeExecutionRequest(RemoteExecutionRequest[CodeExecutionResponse]):
456
+ namespace: ClassVar[Namespace] = "code_execution"
457
+
458
+ language: SupportedLanguage
459
+ content: str
460
+ timeout: int
461
+
462
+
463
+ class StreamingCodeExecutionRequest(
464
+ RemoteExecutionRequest[
465
+ StreamingCodeExecutionResponseChunk | StreamingCodeExecutionResponse
466
+ ]
467
+ ):
468
+ namespace: ClassVar[Namespace] = "streaming_code_execution"
469
+
470
+ language: SupportedLanguage
471
+ content: str
472
+ timeout: int
473
+
474
+
475
+ class FileWriteRequest(RemoteExecutionRequest[FileWriteResponse]):
476
+ namespace: ClassVar[Namespace] = "file_write"
477
+
478
+ file_path: str
479
+ # Note we don't use SupportedLanguage here because we don't
480
+ # require language-specific execution support for file writes
481
+ language: str
482
+ write_strategy: FileWriteStrategyName
483
+ content: str
484
+
485
+
486
+ class ListFilesRequest(RemoteExecutionRequest[ListFilesResponse]):
487
+ namespace: ClassVar[Namespace] = "list_files"
488
+
489
+ directory: str
490
+
491
+
492
+ class CreateCheckpointRequest(RemoteExecutionRequest[CreateCheckpointResponse]):
493
+ namespace: ClassVar[Namespace] = "create_checkpoint"
494
+
495
+ last_checkpoint_head_commit: str | None = None
496
+ last_checkpoint_uncommitted_changes_commit: str | None = None
497
+
498
+
499
+ class RollbackToCheckpointRequest(RemoteExecutionRequest[RollbackToCheckpointResponse]):
500
+ namespace: ClassVar[Namespace] = "rollback_to_checkpoint"
501
+
502
+ head_commit: str
503
+ uncommitted_changes_commit: str | None
504
+
505
+
506
+ ### Commands
507
+
508
+
509
+ ### Command Response Types
510
+
511
+
512
+ class CommandResponse(RemoteExecutionResponse):
513
+ namespace: ClassVar[Namespace] = "command"
514
+
515
+ content: str
516
+ content_json: dict[str, Any] = Field(default_factory=dict)
517
+ subcommand: str = "unknown"
518
+ truncated: bool = False
519
+
520
+
521
+ ### Command Request Types
522
+
523
+
524
+ class CommandRequest(RemoteExecutionRequest[CommandResponse]):
525
+ namespace: ClassVar[Namespace] = "command"
526
+
527
+ data: CommandDataType = Field(..., discriminator="type")
528
+
529
+
530
+ RemoteExecutionRequestType = (
531
+ CodeExecutionRequest
532
+ | FileWriteRequest
533
+ | ListFilesRequest
534
+ | CommandRequest
535
+ | StreamingCodeExecutionRequest
536
+ | CreateCheckpointRequest
537
+ | RollbackToCheckpointRequest
538
+ )
539
+
540
+ RemoteExecutionResponseType = (
541
+ CodeExecutionResponse
542
+ | StreamingCodeExecutionResponseChunk
543
+ | StreamingCodeExecutionResponse
544
+ | FileWriteResponse
545
+ | ListFilesResponse
546
+ | CommandResponse
547
+ | ErrorResponse
548
+ | CreateCheckpointResponse
549
+ | RollbackToCheckpointResponse
550
+ )
551
+
552
+ StreamingResponseType = (
553
+ StreamingCodeExecutionResponseChunk | StreamingCodeExecutionResponse | ErrorResponse
554
+ )
555
+
556
+ STREAMING_NAMESPACES = [
557
+ "streaming_code_execution",
558
+ "streaming_code_execution_chunk",
559
+ ]
560
+
561
+
562
+ class ChatMode(str, Enum):
563
+ DEFAULT = "DEFAULT" # chat just with model
564
+ CLI = "CLI"
565
+ CLOUD = "CLOUD" # chat with cloud devbox
566
+ CODEBASE = "CODEBASE" # chat with codebase
567
+ DATABASE = "DATABASE" # chat with database connection
568
+ WORKFLOW = "WORKFLOW"
569
+
570
+ @classmethod
571
+ def requires_cli(cls, mode: "ChatMode") -> bool:
572
+ return mode not in [cls.DATABASE]
573
+
574
+
575
+ class ChatSource(str, Enum):
576
+ CLI_SHELL = "CLI_SHELL"
577
+ CLI_RUN = "CLI_RUN"
578
+ WEB = "WEB"
579
+ DESKTOP_APP = "DESKTOP_APP"
580
+ VSCODE_EXTENSION = "VSCODE_EXTENSION"
581
+ SLACK_APP = "SLACK_APP"
582
+ SENTRY_APP = "SENTRY_APP"
583
+
584
+
585
+ class CLIConnectedState(BaseModel):
586
+ chat_uuid: str
587
+ connected: bool
588
+ last_connected_at: datetime.datetime | None
589
+ connection_latency_ms: int | None
590
+ system_info: SystemInfo | None
591
+ exponent_version: str | None = None
592
+ editable_installation: bool = False
593
+
594
+
595
+ class DevboxConnectedState(str, Enum):
596
+ # The chat has been initialized, but the devbox is still loading
597
+ DEVBOX_LOADING = "DEVBOX_LOADING"
598
+ # CLI is connected and running on devbox
599
+ CONNECTED = "CONNECTED"
600
+ # Devbox has an error
601
+ DEVBOX_ERROR = "DEVBOX_ERROR"
602
+ # Devbox is going to idle
603
+ PAUSING = "PAUSING"
604
+ # Devbox has been paused and is not running
605
+ PAUSED = "PAUSED"
606
+ # Dev box is starting up. Sandbox exists but devbox is not running
607
+ RESUMING = "RESUMING"
608
+
609
+
610
+ class CloudConnectedState(BaseModel):
611
+ chat_uuid: str
612
+ connected_state: DevboxConnectedState
613
+ last_connected_at: datetime.datetime | None
614
+ system_info: SystemInfo | None
615
+
616
+
617
+ class CLIErrorLog(BaseModel):
618
+ event_data: str
619
+ timestamp: datetime.datetime = datetime.datetime.now()
620
+ attachment_data: str | None = None
621
+ version: str | None = None
622
+ chat_uuid: str | None = None
623
+
624
+ @property
625
+ def loaded_event_data(self) -> Any | None:
626
+ try:
627
+ return json.loads(self.event_data)
628
+ except json.JSONDecodeError:
629
+ return None
630
+
631
+ @property
632
+ def attachment_bytes(self) -> bytes | None:
633
+ if not self.attachment_data:
634
+ return None
635
+ return self.attachment_data.encode()