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.
- {indent-0.1.5 → indent-0.1.7}/PKG-INFO +1 -1
- indent-0.1.7/exponent/__init__.py +1 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/cli_rpc_types.py +32 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/client.py +9 -0
- indent-0.1.7/exponent/core/remote_execution/http_fetch.py +87 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/tool_execution.py +81 -0
- {indent-0.1.5 → indent-0.1.7}/pyproject.toml +1 -1
- indent-0.1.5/exponent/__init__.py +0 -1
- {indent-0.1.5 → indent-0.1.7}/.gitignore +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/cli.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/cloud_commands.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/common.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/config_commands.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/github_app_commands.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/listen_commands.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/run_commands.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/settings.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/shell_commands.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/theme.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/types.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/upgrade.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/utils.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/commands/workflow_commands.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/config.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/__init__.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/client.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/cloud_config_queries.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/get_chats_query.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/github_config_queries.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/mutations.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/queries.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/graphql/subscriptions.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/checkpoints.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/code_execution.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/error_info.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/exceptions.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/file_write.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/files.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/git.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/languages/python_execution.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/languages/shell_streaming.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/languages/types.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/session.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/system_context.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/truncation.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/types.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/remote_execution/utils.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/types/__init__.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/types/command_data.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/types/event_types.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/types/generated/__init__.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/core/types/generated/strategy_info.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/migration-docs/login.md +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/py.typed +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/utils/__init__.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/utils/colors.py +0 -0
- {indent-0.1.5 → indent-0.1.7}/exponent/utils/version.py +0 -0
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|