indent 0.1.5__tar.gz → 0.1.7__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.

Potentially problematic release.


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

Files changed (57) hide show
  1. {indent-0.1.5 → indent-0.1.7}/PKG-INFO +1 -1
  2. indent-0.1.7/exponent/__init__.py +1 -0
  3. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/cli_rpc_types.py +32 -0
  4. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/client.py +9 -0
  5. indent-0.1.7/exponent/core/remote_execution/http_fetch.py +87 -0
  6. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/tool_execution.py +81 -0
  7. {indent-0.1.5 → indent-0.1.7}/pyproject.toml +1 -1
  8. indent-0.1.5/exponent/__init__.py +0 -1
  9. {indent-0.1.5 → indent-0.1.7}/.gitignore +0 -0
  10. {indent-0.1.5 → indent-0.1.7}/exponent/cli.py +0 -0
  11. {indent-0.1.5 → indent-0.1.7}/exponent/commands/cloud_commands.py +0 -0
  12. {indent-0.1.5 → indent-0.1.7}/exponent/commands/common.py +0 -0
  13. {indent-0.1.5 → indent-0.1.7}/exponent/commands/config_commands.py +0 -0
  14. {indent-0.1.5 → indent-0.1.7}/exponent/commands/github_app_commands.py +0 -0
  15. {indent-0.1.5 → indent-0.1.7}/exponent/commands/listen_commands.py +0 -0
  16. {indent-0.1.5 → indent-0.1.7}/exponent/commands/run_commands.py +0 -0
  17. {indent-0.1.5 → indent-0.1.7}/exponent/commands/settings.py +0 -0
  18. {indent-0.1.5 → indent-0.1.7}/exponent/commands/shell_commands.py +0 -0
  19. {indent-0.1.5 → indent-0.1.7}/exponent/commands/theme.py +0 -0
  20. {indent-0.1.5 → indent-0.1.7}/exponent/commands/types.py +0 -0
  21. {indent-0.1.5 → indent-0.1.7}/exponent/commands/upgrade.py +0 -0
  22. {indent-0.1.5 → indent-0.1.7}/exponent/commands/utils.py +0 -0
  23. {indent-0.1.5 → indent-0.1.7}/exponent/commands/workflow_commands.py +0 -0
  24. {indent-0.1.5 → indent-0.1.7}/exponent/core/config.py +0 -0
  25. {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/__init__.py +0 -0
  26. {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/client.py +0 -0
  27. {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/cloud_config_queries.py +0 -0
  28. {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/get_chats_query.py +0 -0
  29. {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/github_config_queries.py +0 -0
  30. {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/mutations.py +0 -0
  31. {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/queries.py +0 -0
  32. {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/subscriptions.py +0 -0
  33. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/checkpoints.py +0 -0
  34. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/code_execution.py +0 -0
  35. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/error_info.py +0 -0
  36. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/exceptions.py +0 -0
  37. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/file_write.py +0 -0
  38. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/files.py +0 -0
  39. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/git.py +0 -0
  40. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/languages/python_execution.py +0 -0
  41. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/languages/shell_streaming.py +0 -0
  42. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/languages/types.py +0 -0
  43. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/session.py +0 -0
  44. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/system_context.py +0 -0
  45. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/truncation.py +0 -0
  46. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/types.py +0 -0
  47. {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/utils.py +0 -0
  48. {indent-0.1.5 → indent-0.1.7}/exponent/core/types/__init__.py +0 -0
  49. {indent-0.1.5 → indent-0.1.7}/exponent/core/types/command_data.py +0 -0
  50. {indent-0.1.5 → indent-0.1.7}/exponent/core/types/event_types.py +0 -0
  51. {indent-0.1.5 → indent-0.1.7}/exponent/core/types/generated/__init__.py +0 -0
  52. {indent-0.1.5 → indent-0.1.7}/exponent/core/types/generated/strategy_info.py +0 -0
  53. {indent-0.1.5 → indent-0.1.7}/exponent/migration-docs/login.md +0 -0
  54. {indent-0.1.5 → indent-0.1.7}/exponent/py.typed +0 -0
  55. {indent-0.1.5 → indent-0.1.7}/exponent/utils/__init__.py +0 -0
  56. {indent-0.1.5 → indent-0.1.7}/exponent/utils/colors.py +0 -0
  57. {indent-0.1.5 → indent-0.1.7}/exponent/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: indent
3
- Version: 0.1.5
3
+ Version: 0.1.7
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
@@ -0,0 +1 @@
1
+ __version__ = "0.1.7" # Keep in sync with pyproject.toml
@@ -104,6 +104,20 @@ class GrepToolResult(ToolResult, tag=GREP_TOOL_NAME):
104
104
  truncated: bool = False
105
105
 
106
106
 
107
+ EDIT_TOOL_NAME = "edit"
108
+
109
+
110
+ class EditToolInput(ToolInput, tag=EDIT_TOOL_NAME):
111
+ file_path: str
112
+ old_string: str
113
+ new_string: str
114
+ replace_all: bool = False
115
+
116
+
117
+ class EditToolResult(ToolResult, tag=EDIT_TOOL_NAME):
118
+ message: str
119
+
120
+
107
121
  BASH_TOOL_NAME = "bash"
108
122
 
109
123
 
@@ -125,12 +139,27 @@ class BashToolResult(ToolResult, tag=BASH_TOOL_NAME):
125
139
  stopped_by_user: bool
126
140
 
127
141
 
142
+
143
+ class HttpRequest(msgspec.Struct, tag="http_fetch_cli"):
144
+ url: str
145
+ method: str = "GET"
146
+ headers: dict[str, str] | None = None
147
+ timeout: int | None = None
148
+
149
+ class HttpResponse(msgspec.Struct, tag="http_fetch_cli"):
150
+ status_code: int | None = None
151
+ content: str | None = None
152
+ error_message: str | None = None
153
+ duration_ms: int | None = None
154
+ headers: dict[str, str] | None = None
155
+
128
156
  ToolInputType = (
129
157
  ReadToolInput
130
158
  | WriteToolInput
131
159
  | ListToolInput
132
160
  | GlobToolInput
133
161
  | GrepToolInput
162
+ | EditToolInput
134
163
  | BashToolInput
135
164
  )
136
165
  PartialToolResultType = PartialBashToolResult
@@ -141,6 +170,7 @@ ToolResultType = (
141
170
  | ListToolResult
142
171
  | GlobToolResult
143
172
  | GrepToolResult
173
+ | EditToolResult
144
174
  | BashToolResult
145
175
  | ErrorToolResult
146
176
  )
@@ -180,6 +210,7 @@ class CliRpcRequest(msgspec.Struct):
180
210
  ToolExecutionRequest
181
211
  | GetAllFilesRequest
182
212
  | TerminateRequest
213
+ | HttpRequest
183
214
  | BatchToolExecutionRequest
184
215
  )
185
216
 
@@ -200,4 +231,5 @@ class CliRpcResponse(msgspec.Struct):
200
231
  | ErrorResponse
201
232
  | TerminateResponse
202
233
  | BatchToolExecutionResponse
234
+ | HttpResponse
203
235
  )
@@ -29,6 +29,8 @@ from exponent.core.remote_execution.cli_rpc_types import (
29
29
  ErrorResponse,
30
30
  GetAllFilesRequest,
31
31
  GetAllFilesResponse,
32
+ HttpResponse,
33
+ HttpRequest,
32
34
  TerminateRequest,
33
35
  TerminateResponse,
34
36
  ToolExecutionRequest,
@@ -39,6 +41,7 @@ from exponent.core.remote_execution.code_execution import (
39
41
  execute_code_streaming,
40
42
  )
41
43
  from exponent.core.remote_execution.files import file_walk
44
+ from exponent.core.remote_execution.http_fetch import fetch_http_content
42
45
  from exponent.core.remote_execution.session import (
43
46
  RemoteExecutionClientSession,
44
47
  get_session,
@@ -488,6 +491,12 @@ class RemoteExecutionClient:
488
491
  tool_results=results,
489
492
  ),
490
493
  )
494
+ elif isinstance(request.request, HttpRequest):
495
+ http_response = await fetch_http_content(request.request)
496
+ return CliRpcResponse(
497
+ request_id=request.request_id,
498
+ response=http_response,
499
+ )
491
500
  elif isinstance(request.request, TerminateRequest):
492
501
  raise ValueError(
493
502
  "TerminateRequest should not be handled by handle_request"
@@ -0,0 +1,87 @@
1
+ """HTTP fetch implementation for remote execution client."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from exponent.core.remote_execution.cli_rpc_types import (
9
+ HttpResponse,
10
+ HttpRequest,
11
+ )
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ DEFAULT_TIMEOUT = 30.0
16
+ DEFAULT_USER_AGENT = "Indent-HTTP-Client/1.0"
17
+
18
+
19
+ async def fetch_http_content(http_request: HttpRequest) -> HttpResponse:
20
+ """
21
+ Fetch content from an HTTP URL and return the response.
22
+
23
+ Args:
24
+ http_request: HttpRequest containing URL, method, headers, and timeout
25
+
26
+ Returns:
27
+ HttpResponse with status code, content, and error message if any
28
+ """
29
+ logger.info(f"Fetching {http_request.method} {http_request.url}")
30
+
31
+ try:
32
+ # Set up timeout
33
+ timeout = http_request.timeout if http_request.timeout is not None else DEFAULT_TIMEOUT
34
+
35
+ # Set up headers with default User-Agent
36
+ headers = http_request.headers or {}
37
+ if "User-Agent" not in headers:
38
+ headers["User-Agent"] = DEFAULT_USER_AGENT
39
+
40
+ # Create HTTP client with timeout
41
+ async with httpx.AsyncClient(timeout=timeout) as client:
42
+ # Make the HTTP request
43
+ response = await client.request(
44
+ method=http_request.method,
45
+ url=http_request.url,
46
+ headers=headers,
47
+ )
48
+
49
+ # Get response content as text
50
+ try:
51
+ content = response.text
52
+ except UnicodeDecodeError:
53
+ # If content can't be decoded as text, provide a fallback
54
+ content = f"Binary content ({len(response.content)} bytes)"
55
+ logger.warning(f"Could not decode response content as text for {http_request.url}")
56
+
57
+ logger.info(f"HTTP {http_request.method} {http_request.url} -> {response.status_code}")
58
+
59
+ return HttpResponse(
60
+ status_code=response.status_code,
61
+ content=content,
62
+ error_message=None,
63
+ )
64
+
65
+ except httpx.TimeoutException:
66
+ error_msg = f"Request to {http_request.url} timed out after {timeout} seconds"
67
+ return HttpResponse(
68
+ status_code=None,
69
+ content="",
70
+ error_message=error_msg,
71
+ )
72
+
73
+ except httpx.RequestError as e:
74
+ error_msg = f"Request error for {http_request.url}: {str(e)}"
75
+ return HttpResponse(
76
+ status_code=None,
77
+ content="",
78
+ error_message=error_msg,
79
+ )
80
+
81
+ except Exception as e:
82
+ error_msg = f"Unexpected error fetching {http_request.url}: {str(e)}"
83
+ return HttpResponse(
84
+ status_code=None,
85
+ content="",
86
+ error_message=error_msg,
87
+ )
@@ -9,6 +9,8 @@ from exponent.core.remote_execution import files
9
9
  from exponent.core.remote_execution.cli_rpc_types import (
10
10
  BashToolInput,
11
11
  BashToolResult,
12
+ EditToolInput,
13
+ EditToolResult,
12
14
  ErrorToolResult,
13
15
  GlobToolInput,
14
16
  GlobToolResult,
@@ -51,6 +53,8 @@ async def execute_tool(
51
53
  return await execute_glob_files(tool_input, working_directory)
52
54
  elif isinstance(tool_input, GrepToolInput):
53
55
  return await execute_grep_files(tool_input, working_directory)
56
+ elif isinstance(tool_input, EditToolInput):
57
+ return await execute_edit_file(tool_input, working_directory)
54
58
  elif isinstance(tool_input, BashToolInput):
55
59
  raise ValueError("Bash tool input should be handled by execute_bash_tool")
56
60
  else:
@@ -190,6 +194,83 @@ async def execute_write_file(
190
194
  return WriteToolResult(message=result)
191
195
 
192
196
 
197
+ async def execute_edit_file( # noqa: PLR0911
198
+ tool_input: EditToolInput, working_directory: str
199
+ ) -> EditToolResult | ErrorToolResult:
200
+ # Validate absolute path requirement
201
+ if not tool_input.file_path.startswith("/"):
202
+ return ErrorToolResult(
203
+ error_message=f"File path must be absolute, got relative path: {tool_input.file_path}"
204
+ )
205
+
206
+ file = AsyncPath(working_directory, tool_input.file_path)
207
+
208
+ try:
209
+ exists = await file.exists()
210
+ except (OSError, PermissionError) as e:
211
+ return ErrorToolResult(error_message=f"Cannot access file: {e!s}")
212
+
213
+ if not exists:
214
+ return ErrorToolResult(error_message="File not found")
215
+
216
+ try:
217
+ if await file.is_dir():
218
+ return ErrorToolResult(
219
+ error_message=f"{await file.absolute()} is a directory"
220
+ )
221
+ except (OSError, PermissionError) as e:
222
+ return ErrorToolResult(error_message=f"Cannot check file type: {e!s}")
223
+
224
+ try:
225
+ # Read the entire file without truncation limits
226
+ content = await safe_read_file(file)
227
+ except PermissionError:
228
+ return ErrorToolResult(
229
+ error_message=f"Permission denied: cannot read {tool_input.file_path}"
230
+ )
231
+ except UnicodeDecodeError:
232
+ return ErrorToolResult(
233
+ error_message="File appears to be binary or has invalid text encoding"
234
+ )
235
+ except Exception as e: # noqa: BLE001
236
+ return ErrorToolResult(error_message=f"Error reading file: {e!s}")
237
+
238
+ # Check if search text exists
239
+ if tool_input.old_string not in content:
240
+ return ErrorToolResult(
241
+ error_message=f"Search text not found in {tool_input.file_path}"
242
+ )
243
+
244
+ # Check if old_string and new_string are identical
245
+ if tool_input.old_string == tool_input.new_string:
246
+ return ErrorToolResult(error_message="Old string and new string are identical")
247
+
248
+ # Check uniqueness if replace_all is False
249
+ if not tool_input.replace_all:
250
+ occurrences = content.count(tool_input.old_string)
251
+ if occurrences > 1:
252
+ return ErrorToolResult(
253
+ error_message=f"String '{tool_input.old_string}' appears {occurrences} times in file. Use a larger context or replace_all=True"
254
+ )
255
+
256
+ # Perform replacement
257
+ if tool_input.replace_all:
258
+ new_content = content.replace(tool_input.old_string, tool_input.new_string)
259
+ else:
260
+ # Replace only the first occurrence
261
+ new_content = content.replace(tool_input.old_string, tool_input.new_string, 1)
262
+
263
+ # Write back to file
264
+ try:
265
+ path = Path(working_directory, tool_input.file_path)
266
+ await execute_full_file_rewrite(path, new_content, working_directory)
267
+ return EditToolResult(
268
+ message=f"Successfully replaced text in {tool_input.file_path}"
269
+ )
270
+ except Exception as e: # noqa: BLE001
271
+ return ErrorToolResult(error_message=f"Error writing file: {e!s}")
272
+
273
+
193
274
  async def execute_list_files(
194
275
  tool_input: ListToolInput, working_directory: str
195
276
  ) -> ListToolResult | ErrorToolResult:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "indent"
7
- version = "0.1.5"
7
+ version = "0.1.7"
8
8
  description = "Indent is an AI Pair Programmer"
9
9
  authors = [{ name = "Sashank Thupukari", email = "sashank@exponent.run" }]
10
10
  requires-python = ">=3.10,<3.13"
@@ -1 +0,0 @@
1
- __version__ = "0.1.5" # Keep in sync with pyproject.toml
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes