indent 0.1.13__py3-none-any.whl → 0.1.28__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 (31) hide show
  1. exponent/__init__.py +2 -2
  2. exponent/cli.py +0 -2
  3. exponent/commands/cloud_commands.py +2 -87
  4. exponent/commands/common.py +25 -40
  5. exponent/commands/config_commands.py +0 -87
  6. exponent/commands/run_commands.py +5 -2
  7. exponent/core/config.py +1 -1
  8. exponent/core/container_build/__init__.py +0 -0
  9. exponent/core/container_build/types.py +25 -0
  10. exponent/core/graphql/mutations.py +2 -31
  11. exponent/core/graphql/queries.py +0 -3
  12. exponent/core/remote_execution/cli_rpc_types.py +201 -5
  13. exponent/core/remote_execution/client.py +355 -92
  14. exponent/core/remote_execution/code_execution.py +26 -7
  15. exponent/core/remote_execution/default_env.py +31 -0
  16. exponent/core/remote_execution/languages/shell_streaming.py +11 -6
  17. exponent/core/remote_execution/port_utils.py +73 -0
  18. exponent/core/remote_execution/system_context.py +2 -0
  19. exponent/core/remote_execution/terminal_session.py +517 -0
  20. exponent/core/remote_execution/terminal_types.py +29 -0
  21. exponent/core/remote_execution/tool_execution.py +228 -18
  22. exponent/core/remote_execution/tool_type_utils.py +39 -0
  23. exponent/core/remote_execution/truncation.py +9 -1
  24. exponent/core/remote_execution/types.py +71 -19
  25. exponent/utils/version.py +8 -7
  26. {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/METADATA +5 -2
  27. {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/RECORD +29 -24
  28. exponent/commands/workflow_commands.py +0 -111
  29. exponent/core/graphql/github_config_queries.py +0 -56
  30. {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/WHEEL +0 -0
  31. {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,10 @@
1
+ import asyncio
1
2
  import logging
2
3
  import uuid
3
4
  from collections.abc import Callable
4
5
  from pathlib import Path
5
6
  from time import time
7
+ from typing import TYPE_CHECKING
6
8
 
7
9
  from anyio import Path as AsyncPath
8
10
 
@@ -10,6 +12,8 @@ from exponent.core.remote_execution import files
10
12
  from exponent.core.remote_execution.cli_rpc_types import (
11
13
  BashToolInput,
12
14
  BashToolResult,
15
+ DownloadArtifactToolInput,
16
+ DownloadArtifactToolResult,
13
17
  EditToolInput,
14
18
  EditToolResult,
15
19
  ErrorToolResult,
@@ -19,22 +23,32 @@ from exponent.core.remote_execution.cli_rpc_types import (
19
23
  GrepToolResult,
20
24
  ListToolInput,
21
25
  ListToolResult,
26
+ ReadToolArtifactResult,
22
27
  ReadToolInput,
23
28
  ReadToolResult,
24
29
  ToolInputType,
25
30
  ToolResultType,
31
+ UploadArtifactToolInput,
32
+ UploadArtifactToolResult,
26
33
  WriteToolInput,
27
34
  WriteToolResult,
28
35
  )
36
+
37
+ if TYPE_CHECKING:
38
+ from exponent.core.remote_execution.client import RemoteExecutionClient
39
+ import urllib.request
40
+
41
+ import aiohttp
42
+
43
+ from exponent.core.remote_execution.cli_rpc_types import (
44
+ StreamingCodeExecutionRequest,
45
+ StreamingCodeExecutionResponse,
46
+ )
29
47
  from exponent.core.remote_execution.code_execution import (
30
48
  execute_code_streaming,
31
49
  )
32
50
  from exponent.core.remote_execution.file_write import execute_full_file_rewrite
33
51
  from exponent.core.remote_execution.truncation import truncate_tool_result
34
- from exponent.core.remote_execution.types import (
35
- StreamingCodeExecutionRequest,
36
- StreamingCodeExecutionResponse,
37
- )
38
52
  from exponent.core.remote_execution.utils import (
39
53
  assert_unreachable,
40
54
  safe_get_file_metadata,
@@ -45,10 +59,12 @@ logger = logging.getLogger(__name__)
45
59
 
46
60
 
47
61
  async def execute_tool(
48
- tool_input: ToolInputType, working_directory: str
62
+ tool_input: ToolInputType,
63
+ working_directory: str,
64
+ upload_client: "RemoteExecutionClient | None" = None,
49
65
  ) -> ToolResultType:
50
66
  if isinstance(tool_input, ReadToolInput):
51
- return await execute_read_file(tool_input, working_directory)
67
+ return await execute_read_file(tool_input, working_directory, upload_client)
52
68
  elif isinstance(tool_input, WriteToolInput):
53
69
  return await execute_write_file(tool_input, working_directory)
54
70
  elif isinstance(tool_input, ListToolInput):
@@ -59,6 +75,10 @@ async def execute_tool(
59
75
  return await execute_grep_files(tool_input, working_directory)
60
76
  elif isinstance(tool_input, EditToolInput):
61
77
  return await execute_edit_file(tool_input, working_directory)
78
+ elif isinstance(tool_input, DownloadArtifactToolInput):
79
+ return await execute_download_artifact(tool_input, working_directory)
80
+ elif isinstance(tool_input, UploadArtifactToolInput):
81
+ return await execute_upload_artifact(tool_input, working_directory)
62
82
  elif isinstance(tool_input, BashToolInput):
63
83
  raise ValueError("Bash tool input should be handled by execute_bash_tool")
64
84
  else:
@@ -69,8 +89,19 @@ def truncate_result[T: ToolResultType](tool_result: T) -> T:
69
89
  return truncate_tool_result(tool_result)
70
90
 
71
91
 
72
- async def execute_read_file( # noqa: PLR0911
73
- tool_input: ReadToolInput, working_directory: str
92
+ def is_image_file(file_path: str) -> tuple[bool, str | None]:
93
+ ext = Path(file_path).suffix.lower()
94
+ if ext == ".png":
95
+ return (True, "image/png")
96
+ elif ext in [".jpg", ".jpeg"]:
97
+ return (True, "image/jpeg")
98
+ return (False, None)
99
+
100
+
101
+ async def execute_read_file( # noqa: PLR0911, PLR0915
102
+ tool_input: ReadToolInput,
103
+ working_directory: str,
104
+ upload_client: "RemoteExecutionClient | None" = None,
74
105
  ) -> ReadToolResult | ErrorToolResult:
75
106
  # Validate absolute path requirement
76
107
  if not tool_input.file_path.startswith("/"):
@@ -82,16 +113,49 @@ async def execute_read_file( # noqa: PLR0911
82
113
  offset = tool_input.offset if tool_input.offset is not None else 0
83
114
  limit = tool_input.limit if tool_input.limit is not None else 2000
84
115
 
85
- if offset < 0:
86
- return ErrorToolResult(
87
- error_message=f"Offset must be non-negative, got: {offset}"
88
- )
89
-
90
116
  if limit <= 0:
91
117
  return ErrorToolResult(error_message=f"Limit must be positive, got: {limit}")
92
118
 
93
119
  file = AsyncPath(working_directory, tool_input.file_path)
94
120
 
121
+ # Check if this is an image file and we have an upload client
122
+ is_image, media_type = is_image_file(tool_input.file_path)
123
+ if is_image and media_type and upload_client is not None:
124
+ try:
125
+ file_name = Path(tool_input.file_path).name
126
+ s3_key = f"images/{uuid.uuid4()}/{file_name}"
127
+
128
+ upload_response = await upload_client.request_upload_url(s3_key, media_type)
129
+
130
+ f = await file.open("rb")
131
+ async with f:
132
+ file_data = await f.read()
133
+
134
+ def _upload() -> int:
135
+ req = urllib.request.Request(
136
+ upload_response.upload_url,
137
+ data=file_data,
138
+ headers={"Content-Type": media_type},
139
+ method="PUT",
140
+ )
141
+ with urllib.request.urlopen(req) as resp:
142
+ status: int = resp.status
143
+ return status
144
+
145
+ status = await asyncio.to_thread(_upload)
146
+ if status != 200:
147
+ raise RuntimeError(f"Upload failed with status {status}")
148
+
149
+ return ReadToolResult(
150
+ artifact=ReadToolArtifactResult(
151
+ s3_uri=upload_response.s3_uri,
152
+ file_path=tool_input.file_path,
153
+ media_type=media_type,
154
+ )
155
+ )
156
+ except Exception as e:
157
+ return ErrorToolResult(error_message=f"Failed to upload image to S3: {e!s}")
158
+
95
159
  try:
96
160
  exists = await file.exists()
97
161
  except (OSError, PermissionError) as e:
@@ -138,8 +202,8 @@ async def execute_read_file( # noqa: PLR0911
138
202
  content_lines = content.splitlines(keepends=True)
139
203
  total_lines = len(content_lines)
140
204
 
141
- # Handle offset beyond file length
142
- if offset >= total_lines:
205
+ # Handle offset beyond file length for positive offsets
206
+ if offset >= 0 and offset >= total_lines:
143
207
  return ReadToolResult(
144
208
  content="",
145
209
  num_lines=0,
@@ -148,8 +212,26 @@ async def execute_read_file( # noqa: PLR0911
148
212
  metadata=metadata,
149
213
  )
150
214
 
151
- # Apply offset and limit
152
- content_lines = content_lines[offset : offset + limit]
215
+ # Use Python's native slicing - it handles negative offsets naturally
216
+ # Handle the case where offset + limit < 0 (can't mix negative and non-negative indices)
217
+ if offset < 0 and offset + limit < 0:
218
+ # Both start and end are negative, use negative end index
219
+ end_index = offset + limit
220
+ elif offset < 0 and offset + limit >= 0:
221
+ # Start is negative but end would be positive/zero, slice to end
222
+ end_index = None
223
+ else:
224
+ # Normal case: both indices are non-negative
225
+ end_index = offset + limit
226
+
227
+ content_lines = content_lines[offset:end_index]
228
+
229
+ # Calculate the actual start line for the result
230
+ if offset < 0:
231
+ # For negative offsets, calculate where we actually started
232
+ actual_start_line = max(0, total_lines + offset)
233
+ else:
234
+ actual_start_line = offset
153
235
 
154
236
  # Apply character-level truncation at line boundaries to ensure consistency
155
237
  # This ensures the content field and num_lines field remain in sync
@@ -186,7 +268,7 @@ async def execute_read_file( # noqa: PLR0911
186
268
  return ReadToolResult(
187
269
  content=final_content,
188
270
  num_lines=num_lines,
189
- start_line=offset,
271
+ start_line=actual_start_line,
190
272
  total_lines=total_lines,
191
273
  metadata=metadata,
192
274
  )
@@ -383,3 +465,131 @@ async def execute_bash_tool(
383
465
  timed_out=result.cancelled_for_timeout,
384
466
  stopped_by_user=result.halted,
385
467
  )
468
+
469
+
470
+ async def execute_download_artifact(
471
+ tool_input: DownloadArtifactToolInput, working_directory: str
472
+ ) -> DownloadArtifactToolResult | ErrorToolResult:
473
+ """Download an artifact from S3 using a pre-signed URL."""
474
+
475
+ # Validate absolute path
476
+ if not tool_input.file_path.startswith("/"):
477
+ return ErrorToolResult(
478
+ error_message=f"File path must be absolute, got relative path: {tool_input.file_path}"
479
+ )
480
+
481
+ # Check if file exists and overwrite is False
482
+ file_path = Path(tool_input.file_path)
483
+ if file_path.exists() and not tool_input.overwrite:
484
+ return ErrorToolResult(
485
+ error_message=f"File already exists: {tool_input.file_path}. Set overwrite=True to replace it."
486
+ )
487
+
488
+ try:
489
+ # Download from pre-signed URL
490
+ async with aiohttp.ClientSession() as session:
491
+ async with session.get(tool_input.presigned_url) as response:
492
+ if response.status != 200:
493
+ error_text = await response.text()
494
+ return ErrorToolResult(
495
+ error_message=f"Failed to download artifact: HTTP {response.status} - {error_text}"
496
+ )
497
+
498
+ # Create parent directory if needed
499
+ file_path.parent.mkdir(parents=True, exist_ok=True)
500
+
501
+ # Write file
502
+ content = await response.read()
503
+ file_path.write_bytes(content)
504
+
505
+ file_size = len(content)
506
+
507
+ # Try to generate content preview for text files
508
+ # Attempt to decode as UTF-8 to determine if it's a text file
509
+ content_preview = None
510
+ num_lines = None
511
+ total_lines = None
512
+ truncated = False
513
+
514
+ try:
515
+ text_content = content.decode("utf-8")
516
+ lines = text_content.splitlines()
517
+ total_lines = len(lines)
518
+
519
+ # Show first 50 lines
520
+ preview_limit = 50
521
+ if len(lines) > preview_limit:
522
+ preview_lines = lines[:preview_limit]
523
+ truncated = True
524
+ num_lines = preview_limit
525
+ else:
526
+ preview_lines = lines
527
+ num_lines = len(lines)
528
+
529
+ content_preview = "\n".join(preview_lines)
530
+ except UnicodeDecodeError:
531
+ # Binary file, skip preview
532
+ pass
533
+
534
+ return DownloadArtifactToolResult(
535
+ file_path=tool_input.file_path,
536
+ artifact_id=tool_input.artifact_id,
537
+ file_size_bytes=file_size,
538
+ content_preview=content_preview,
539
+ num_lines=num_lines,
540
+ total_lines=total_lines,
541
+ truncated=truncated,
542
+ )
543
+
544
+ except Exception as e:
545
+ logger.exception("Failed to download artifact")
546
+ return ErrorToolResult(error_message=f"Failed to download artifact: {e!s}")
547
+
548
+
549
+ async def execute_upload_artifact(
550
+ tool_input: UploadArtifactToolInput, working_directory: str
551
+ ) -> UploadArtifactToolResult | ErrorToolResult:
552
+ """Upload an artifact to S3 using a pre-signed URL."""
553
+
554
+ # Validate absolute path
555
+ if not tool_input.file_path.startswith("/"):
556
+ return ErrorToolResult(
557
+ error_message=f"File path must be absolute, got relative path: {tool_input.file_path}"
558
+ )
559
+
560
+ # Check if file exists
561
+ file_path = Path(tool_input.file_path)
562
+ if not file_path.exists():
563
+ return ErrorToolResult(error_message=f"File not found: {tool_input.file_path}")
564
+
565
+ if not file_path.is_file():
566
+ return ErrorToolResult(
567
+ error_message=f"Path is not a file: {tool_input.file_path}"
568
+ )
569
+
570
+ try:
571
+ # Read file
572
+ content = file_path.read_bytes()
573
+ file_size = len(content)
574
+
575
+ # Upload to pre-signed URL
576
+ async with aiohttp.ClientSession() as session:
577
+ headers = {"Content-Type": tool_input.content_type}
578
+ async with session.put(
579
+ tool_input.presigned_url, data=content, headers=headers
580
+ ) as response:
581
+ if response.status not in (200, 204):
582
+ error_text = await response.text()
583
+ return ErrorToolResult(
584
+ error_message=f"Failed to upload artifact: HTTP {response.status} - {error_text}"
585
+ )
586
+
587
+ return UploadArtifactToolResult(
588
+ artifact_id=tool_input.artifact_id,
589
+ file_size_bytes=file_size,
590
+ content_type=tool_input.content_type,
591
+ )
592
+
593
+ except Exception as e:
594
+ logger.exception("Failed to upload artifact")
595
+ return ErrorToolResult(error_message=f"Failed to upload artifact: {e!s}")
@@ -0,0 +1,39 @@
1
+ from typing import Any
2
+
3
+ import msgspec
4
+
5
+ from exponent.core.remote_execution.cli_rpc_types import ToolResult
6
+
7
+
8
+ def to_mostly_xml(tool_result: ToolResult) -> str:
9
+ """
10
+ This provides a default textual representation of the tool result. Override it as needed for your tool."""
11
+ d = msgspec.to_builtins(tool_result)
12
+ del d["tool_name"]
13
+ return to_mostly_xml_helper(d)
14
+
15
+
16
+ def to_mostly_xml_helper(
17
+ d: Any,
18
+ ) -> str:
19
+ if isinstance(d, dict):
20
+ # No outer wrapper at top level, each field gets XML tags
21
+ parts = []
22
+ for key, value in d.items():
23
+ if isinstance(value, list):
24
+ # Handle lists with item tags
25
+ list_items = "\n".join(
26
+ f"<item>\n{to_mostly_xml_helper(item)}\n</item>" for item in value
27
+ )
28
+ parts.append(f"<{key}>\n{list_items}\n</{key}>")
29
+ elif isinstance(value, dict):
30
+ # Nested dict
31
+ parts.append(f"<{key}>\n{to_mostly_xml_helper(value)}\n</{key}>")
32
+ else:
33
+ # Scalar value
34
+ parts.append(f"<{key}>\n{value!s}\n</{key}>")
35
+ return "\n".join(parts)
36
+ elif isinstance(d, list):
37
+ raise ValueError("Lists are not allowed at the top level")
38
+ else:
39
+ return str(d)
@@ -17,7 +17,7 @@ from exponent.core.remote_execution.cli_rpc_types import (
17
17
  )
18
18
  from exponent.core.remote_execution.utils import truncate_output
19
19
 
20
- DEFAULT_CHARACTER_LIMIT = 90_000
20
+ DEFAULT_CHARACTER_LIMIT = 50_000
21
21
  DEFAULT_LIST_ITEM_LIMIT = 1000
22
22
  DEFAULT_LIST_PREVIEW_ITEMS = 10
23
23
 
@@ -173,6 +173,14 @@ class TailTruncation(TruncationStrategy):
173
173
  return replace(result, **updates)
174
174
 
175
175
 
176
+ class NoOpTruncation(TruncationStrategy):
177
+ def should_truncate(self, result: ToolResult) -> bool:
178
+ return False
179
+
180
+ def truncate(self, result: ToolResult) -> ToolResult:
181
+ return result
182
+
183
+
176
184
  class StringListTruncation(TruncationStrategy):
177
185
  """Truncation for lists of strings that limits both number of items and individual string length."""
178
186
 
@@ -49,9 +49,48 @@ class PrReviewWorkflowInput(BaseModel):
49
49
  pr_number: int
50
50
 
51
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
+
52
91
  class WorkflowTriggerRequest(BaseModel):
53
92
  workflow_name: str
54
- workflow_input: PrReviewWorkflowInput
93
+ workflow_input: WorkflowInput
55
94
 
56
95
 
57
96
  class WorkflowTriggerResponse(BaseModel):
@@ -81,6 +120,14 @@ class PythonEnvInfo(BaseModel):
81
120
  provider: Literal["venv", "pyenv", "pipenv", "conda"] | None = "pyenv"
82
121
 
83
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
+
84
131
  class SystemInfo(BaseModel):
85
132
  name: str
86
133
  cwd: str
@@ -88,6 +135,7 @@ class SystemInfo(BaseModel):
88
135
  shell: str
89
136
  git: GitInfo | None
90
137
  python_env: PythonEnvInfo | None
138
+ port_usage: list[PortInfo] | None = None
91
139
 
92
140
 
93
141
  class HeartbeatInfo(BaseModel):
@@ -208,8 +256,18 @@ class PromptAttachment(BaseModel):
208
256
  prompt_content: str
209
257
 
210
258
 
259
+ class SQLAttachment(BaseModel):
260
+ attachment_type: Literal["sql"] = "sql"
261
+ query_content: str
262
+ query_id: str
263
+
264
+
211
265
  MessageAttachment = Annotated[
212
- FileAttachment | URLAttachment | TableSchemaAttachment | PromptAttachment,
266
+ FileAttachment
267
+ | URLAttachment
268
+ | TableSchemaAttachment
269
+ | PromptAttachment
270
+ | SQLAttachment,
213
271
  Field(discriminator="attachment_type"),
214
272
  ]
215
273
 
@@ -509,6 +567,10 @@ class ChatMode(str, Enum):
509
567
  DATABASE = "DATABASE" # chat with database connection
510
568
  WORKFLOW = "WORKFLOW"
511
569
 
570
+ @classmethod
571
+ def requires_cli(cls, mode: "ChatMode") -> bool:
572
+ return mode not in [cls.DATABASE]
573
+
512
574
 
513
575
  class ChatSource(str, Enum):
514
576
  CLI_SHELL = "CLI_SHELL"
@@ -516,6 +578,8 @@ class ChatSource(str, Enum):
516
578
  WEB = "WEB"
517
579
  DESKTOP_APP = "DESKTOP_APP"
518
580
  VSCODE_EXTENSION = "VSCODE_EXTENSION"
581
+ SLACK_APP = "SLACK_APP"
582
+ SENTRY_APP = "SENTRY_APP"
519
583
 
520
584
 
521
585
  class CLIConnectedState(BaseModel):
@@ -529,30 +593,18 @@ class CLIConnectedState(BaseModel):
529
593
 
530
594
 
531
595
  class DevboxConnectedState(str, Enum):
532
- # TODO: Only needed if we create devbox async
533
- INITIALIZED = "INITIALIZED"
534
596
  # The chat has been initialized, but the devbox is still loading
535
597
  DEVBOX_LOADING = "DEVBOX_LOADING"
536
- # Devbox is ready to use but not tied to a chat
537
- DEVBOX_READY = "DEVBOX_READY"
538
598
  # CLI is connected and running on devbox
539
599
  CONNECTED = "CONNECTED"
540
- # CLI has disconnected
541
- # TODO: what condition?
542
- CLI_DISCONNECTED = "CLI_DISCONNECTED"
543
- # CLI has an error, devbox is running
544
- CLI_ERROR = "CLI_ERROR"
545
600
  # Devbox has an error
546
601
  DEVBOX_ERROR = "DEVBOX_ERROR"
547
602
  # Devbox is going to idle
548
- GOING_TO_IDLE = "GOING_TO_IDLE"
549
- # Devbox is idle
550
- IDLE = "IDLE"
551
- # Devbox is going to idle
552
- RESUMING_FROM_IDLE = "RESUMING_FROM_IDLE"
553
- # Devbox_shutdown
554
- # TODO: In theory our terminal state, do we want to name something different?
555
- DEVBOX_SHUTDOWN = "DEVBOX_SHUTDOWN"
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"
556
608
 
557
609
 
558
610
  class CloudConnectedState(BaseModel):
exponent/utils/version.py CHANGED
@@ -135,8 +135,8 @@ def _get_upgrade_command_str(version: str) -> str:
135
135
 
136
136
  def _new_version_str(current_version: str, new_version: str) -> str:
137
137
  return (
138
- f"\n{click.style('A new Exponent version is available:', fg='cyan')} {new_version} (current: {current_version})\n"
139
- f"See {click.style('https://docs.exponent.run/installation', underline=True)} for details.\n"
138
+ f"\n{click.style('A new Indent version is available:', fg='cyan')} {new_version} (current: {current_version})\n"
139
+ f"See {click.style('https://docs.indent.com/help_and_resources/troubleshooting#installation-methods', underline=True)} for details.\n"
140
140
  )
141
141
 
142
142
 
@@ -148,6 +148,7 @@ def _ask_continue_without_upgrading() -> None:
148
148
  if click.confirm("Continue without upgrading?", default=False):
149
149
  click.secho("Using outdated version.", fg="red")
150
150
  else:
151
+ click.secho("Exiting due to outdated version", fg="red")
151
152
  sys.exit(1)
152
153
 
153
154
 
@@ -196,15 +197,15 @@ def upgrade_exponent(
196
197
 
197
198
  if result.returncode != 0:
198
199
  click.secho(
199
- "\nFailed to upgrade Exponent. See https://docs.exponent.run/installation for help, or reach out to team@exponent.run.",
200
+ "\nFailed to upgrade Indent. See https://docs.indent.com/help_and_resources/troubleshooting#installation-methods for help, or reach out to team@indent.com",
200
201
  fg="red",
201
202
  )
202
203
  sys.exit(2)
203
204
 
204
- click.secho(f"Successfully upgraded Exponent to version {new_version}!", fg="green")
205
+ click.secho(f"Successfully upgraded Indent to version {new_version}!", fg="green")
205
206
 
206
- click.echo("Re-run exponent to use the latest version.")
207
- sys.exit(1)
207
+ click.echo("Re-run indent to use the latest version.")
208
+ sys.exit(0)
208
209
 
209
210
 
210
211
  def _upgrade_thread_worker(
@@ -260,7 +261,7 @@ def upgrade_exponent_in_background(
260
261
  return
261
262
 
262
263
  click.secho(
263
- f"\nUpgrading Exponent from {current_version} to {new_version} (this will take effect next time)\n",
264
+ f"\nUpgrading Indent from {current_version} to {new_version} (this will take effect next time)\n",
264
265
  fg="cyan",
265
266
  bold=True,
266
267
  )
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: indent
3
- Version: 0.1.13
3
+ Version: 0.1.28
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
7
+ Requires-Dist: aiohttp>=3.13.2
7
8
  Requires-Dist: anyio<5,>=4.6.0
8
9
  Requires-Dist: async-timeout<5,>=4.0.3; python_version < '3.11'
9
10
  Requires-Dist: beautifulsoup4[chardet]<5,>=4.13.4
@@ -22,6 +23,8 @@ Requires-Dist: msgspec>=0.19.0
22
23
  Requires-Dist: packaging~=24.1
23
24
  Requires-Dist: pip<26,>=25.0.1
24
25
  Requires-Dist: prompt-toolkit<4,>=3.0.36
26
+ Requires-Dist: psutil<7,>=5.9.0
27
+ Requires-Dist: pydantic-ai==0.0.30
25
28
  Requires-Dist: pydantic-settings<3,>=2.2.1
26
29
  Requires-Dist: pydantic[email]<3,>=2.6.4
27
30
  Requires-Dist: pygit2<2,>=1.15.0
@@ -29,7 +32,7 @@ Requires-Dist: python-ripgrep==0.0.9
29
32
  Requires-Dist: pyyaml>=6.0.2
30
33
  Requires-Dist: questionary<3,>=2.0.1
31
34
  Requires-Dist: rapidfuzz<4,>=3.9.0
32
- Requires-Dist: rich<14,>=13.7.1
35
+ Requires-Dist: rich>=13.7.1
33
36
  Requires-Dist: sentry-sdk<3,>=2.1.1
34
37
  Requires-Dist: toml<0.11,>=0.10.2
35
38
  Requires-Dist: websockets~=15.0