indent 0.1.8__tar.gz → 0.1.10__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.8 → indent-0.1.10}/.gitignore +3 -0
- {indent-0.1.8 → indent-0.1.10}/PKG-INFO +1 -1
- indent-0.1.10/exponent/__init__.py +21 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/cli.py +8 -10
- {indent-0.1.8 → indent-0.1.10}/exponent/commands/config_commands.py +10 -10
- {indent-0.1.8 → indent-0.1.10}/exponent/commands/run_commands.py +1 -1
- {indent-0.1.8 → indent-0.1.10}/exponent/commands/upgrade.py +3 -3
- {indent-0.1.8 → indent-0.1.10}/exponent/commands/utils.py +0 -90
- {indent-0.1.8 → indent-0.1.10}/exponent/commands/workflow_commands.py +1 -1
- {indent-0.1.8 → indent-0.1.10}/exponent/core/config.py +1 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/client.py +57 -1
- {indent-0.1.8 → indent-0.1.10}/pyproject.toml +10 -2
- indent-0.1.8/exponent/__init__.py +0 -1
- indent-0.1.8/exponent/commands/github_app_commands.py +0 -211
- indent-0.1.8/exponent/commands/listen_commands.py +0 -96
- indent-0.1.8/exponent/commands/shell_commands.py +0 -2840
- indent-0.1.8/exponent/commands/theme.py +0 -246
- {indent-0.1.8 → indent-0.1.10}/exponent/commands/cloud_commands.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/commands/common.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/commands/settings.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/commands/types.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/graphql/__init__.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/graphql/client.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/graphql/cloud_config_queries.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/graphql/get_chats_query.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/graphql/github_config_queries.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/graphql/mutations.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/graphql/queries.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/graphql/subscriptions.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/checkpoints.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/cli_rpc_types.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/code_execution.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/error_info.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/exceptions.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/file_write.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/files.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/git.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/http_fetch.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/languages/python_execution.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/languages/shell_streaming.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/languages/types.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/session.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/system_context.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/tool_execution.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/truncation.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/types.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/remote_execution/utils.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/types/__init__.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/types/command_data.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/types/event_types.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/types/generated/__init__.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/core/types/generated/strategy_info.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/migration-docs/login.md +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/py.typed +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/utils/__init__.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/utils/colors.py +0 -0
- {indent-0.1.8 → indent-0.1.10}/exponent/utils/version.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.1.10'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 1, 10)
|
|
@@ -4,10 +4,7 @@ from click import Context, HelpFormatter
|
|
|
4
4
|
from exponent.commands.cloud_commands import cloud_cli
|
|
5
5
|
from exponent.commands.common import set_log_level
|
|
6
6
|
from exponent.commands.config_commands import config_cli
|
|
7
|
-
from exponent.commands.
|
|
8
|
-
from exponent.commands.listen_commands import listen_cli
|
|
9
|
-
from exponent.commands.run_commands import run_cli
|
|
10
|
-
from exponent.commands.shell_commands import shell_cli
|
|
7
|
+
from exponent.commands.run_commands import run, run_cli
|
|
11
8
|
from exponent.commands.types import ExponentGroup, exponent_cli_group
|
|
12
9
|
from exponent.commands.upgrade import upgrade_cli
|
|
13
10
|
from exponent.commands.workflow_commands import workflow_cli
|
|
@@ -16,26 +13,27 @@ from exponent.utils.version import (
|
|
|
16
13
|
)
|
|
17
14
|
|
|
18
15
|
|
|
19
|
-
@exponent_cli_group()
|
|
16
|
+
@exponent_cli_group(invoke_without_command=True)
|
|
20
17
|
@click.version_option(get_installed_version(), prog_name="Indent CLI")
|
|
21
18
|
@click.pass_context
|
|
22
19
|
def cli(ctx: Context) -> None:
|
|
23
20
|
"""
|
|
24
21
|
Indent: Your AI pair programmer.
|
|
25
22
|
|
|
26
|
-
Run indent
|
|
23
|
+
Run indent to start (or indent run)
|
|
27
24
|
"""
|
|
28
25
|
set_log_level()
|
|
29
26
|
|
|
27
|
+
# If no command is provided, invoke the 'run' command
|
|
28
|
+
if ctx.invoked_subcommand is None:
|
|
29
|
+
ctx.invoke(run)
|
|
30
|
+
|
|
30
31
|
|
|
31
32
|
sources: list[ExponentGroup] = [
|
|
32
33
|
config_cli, # Configuration commands
|
|
33
34
|
run_cli, # Run AI chat commands
|
|
34
|
-
shell_cli, # Shell interaction commands
|
|
35
35
|
upgrade_cli, # Upgrade-related commands
|
|
36
36
|
cloud_cli, # Cloud commands
|
|
37
|
-
listen_cli, # Listen to chat events
|
|
38
|
-
github_app_cli, # Setup github app installation
|
|
39
37
|
workflow_cli, # Workflow commands
|
|
40
38
|
]
|
|
41
39
|
|
|
@@ -99,7 +97,7 @@ def hidden(ctx: Context) -> None:
|
|
|
99
97
|
with formatter.section("Usage"):
|
|
100
98
|
if ctx.parent and ctx.parent.command:
|
|
101
99
|
formatter.write_usage(
|
|
102
|
-
ctx.parent.command.name or "
|
|
100
|
+
ctx.parent.command.name or "indent", "COMMAND [ARGS]..."
|
|
103
101
|
)
|
|
104
102
|
formatter.write_paragraph()
|
|
105
103
|
with formatter.indentation():
|
|
@@ -36,7 +36,7 @@ from exponent.utils.version import (
|
|
|
36
36
|
|
|
37
37
|
@exponent_cli_group()
|
|
38
38
|
def config_cli() -> None:
|
|
39
|
-
"""Manage
|
|
39
|
+
"""Manage Indent configuration settings."""
|
|
40
40
|
pass
|
|
41
41
|
|
|
42
42
|
|
|
@@ -133,12 +133,12 @@ async def repos_for_github_config_task(
|
|
|
133
133
|
@click.option(
|
|
134
134
|
"--set-git-warning-disabled",
|
|
135
135
|
is_flag=True,
|
|
136
|
-
help="Disable the git warning for running
|
|
136
|
+
help="Disable the git warning for running Indent from a non-git repository",
|
|
137
137
|
)
|
|
138
138
|
@click.option(
|
|
139
139
|
"--set-git-warning-enabled",
|
|
140
140
|
is_flag=True,
|
|
141
|
-
help="Enable the git warning for running
|
|
141
|
+
help="Enable the git warning for running Indent from a non-git repository",
|
|
142
142
|
)
|
|
143
143
|
@click.option(
|
|
144
144
|
"--set-auto-upgrade-enabled",
|
|
@@ -198,7 +198,7 @@ def config(
|
|
|
198
198
|
set_base_api_url_override: str | None = None,
|
|
199
199
|
set_base_ws_url_override: str | None = None,
|
|
200
200
|
) -> None:
|
|
201
|
-
"""Display current
|
|
201
|
+
"""Display current Indent configuration."""
|
|
202
202
|
|
|
203
203
|
num_options_set = sum(
|
|
204
204
|
[
|
|
@@ -230,7 +230,7 @@ def config(
|
|
|
230
230
|
settings.write_settings_to_config_file()
|
|
231
231
|
|
|
232
232
|
click.secho(
|
|
233
|
-
"Git warning enabled.
|
|
233
|
+
"Git warning enabled. Indent will now check for a git repository.\n",
|
|
234
234
|
fg="yellow",
|
|
235
235
|
)
|
|
236
236
|
|
|
@@ -239,7 +239,7 @@ def config(
|
|
|
239
239
|
settings.write_settings_to_config_file()
|
|
240
240
|
|
|
241
241
|
click.secho(
|
|
242
|
-
"Git warning disabled.
|
|
242
|
+
"Git warning disabled. Indent will no longer check for a git repository.\n",
|
|
243
243
|
fg="yellow",
|
|
244
244
|
)
|
|
245
245
|
|
|
@@ -248,7 +248,7 @@ def config(
|
|
|
248
248
|
settings.write_settings_to_config_file()
|
|
249
249
|
|
|
250
250
|
click.secho(
|
|
251
|
-
"Automatic upgrades enabled.
|
|
251
|
+
"Automatic upgrades enabled. Indent will now check for updates.\n",
|
|
252
252
|
fg="yellow",
|
|
253
253
|
)
|
|
254
254
|
|
|
@@ -257,7 +257,7 @@ def config(
|
|
|
257
257
|
settings.write_settings_to_config_file()
|
|
258
258
|
|
|
259
259
|
click.secho(
|
|
260
|
-
"Automatic upgrades disabled.
|
|
260
|
+
"Automatic upgrades disabled. Indent will no longer check for updates.\n",
|
|
261
261
|
fg="yellow",
|
|
262
262
|
)
|
|
263
263
|
|
|
@@ -312,10 +312,10 @@ def config(
|
|
|
312
312
|
|
|
313
313
|
|
|
314
314
|
@config_cli.command()
|
|
315
|
-
@click.option("--key", help="Your
|
|
315
|
+
@click.option("--key", help="Your Indent API Key")
|
|
316
316
|
@use_settings
|
|
317
317
|
def login(settings: Settings, key: str) -> None:
|
|
318
|
-
"""Log in to
|
|
318
|
+
"""Log in to Indent."""
|
|
319
319
|
|
|
320
320
|
if not key:
|
|
321
321
|
redirect_to_login(settings, "provided")
|
|
@@ -68,7 +68,7 @@ def run(
|
|
|
68
68
|
prompt: str | None = None,
|
|
69
69
|
workflow_id: str | None = None,
|
|
70
70
|
) -> None:
|
|
71
|
-
"""Start or reconnect to an
|
|
71
|
+
"""[default] Start or reconnect to an Indent session."""
|
|
72
72
|
check_exponent_version_and_upgrade(settings)
|
|
73
73
|
|
|
74
74
|
if not settings.api_key:
|
|
@@ -6,7 +6,7 @@ from exponent.utils.version import check_exponent_version, upgrade_exponent
|
|
|
6
6
|
|
|
7
7
|
@exponent_cli_group()
|
|
8
8
|
def upgrade_cli() -> None:
|
|
9
|
-
"""Manage
|
|
9
|
+
"""Manage Indent version upgrades."""
|
|
10
10
|
pass
|
|
11
11
|
|
|
12
12
|
|
|
@@ -17,7 +17,7 @@ def upgrade_cli() -> None:
|
|
|
17
17
|
help="Upgrade without prompting for confirmation, if a new version is available.",
|
|
18
18
|
)
|
|
19
19
|
def upgrade(force: bool = False) -> None:
|
|
20
|
-
"""Upgrade
|
|
20
|
+
"""Upgrade Indent to the latest version."""
|
|
21
21
|
if result := check_exponent_version():
|
|
22
22
|
installed_version, latest_version = result
|
|
23
23
|
upgrade_exponent(
|
|
@@ -26,4 +26,4 @@ def upgrade(force: bool = False) -> None:
|
|
|
26
26
|
force=force,
|
|
27
27
|
)
|
|
28
28
|
else:
|
|
29
|
-
click.echo("
|
|
29
|
+
click.echo("Indent is already up to date.")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
|
-
import threading
|
|
5
4
|
import time
|
|
6
5
|
import webbrowser
|
|
7
6
|
|
|
@@ -69,43 +68,6 @@ def launch_exponent_browser(
|
|
|
69
68
|
webbrowser.open(url)
|
|
70
69
|
|
|
71
70
|
|
|
72
|
-
def start_background_event_loop() -> asyncio.AbstractEventLoop:
|
|
73
|
-
def run_event_loop(loop: asyncio.AbstractEventLoop) -> None:
|
|
74
|
-
asyncio.set_event_loop(loop)
|
|
75
|
-
loop.run_forever()
|
|
76
|
-
|
|
77
|
-
loop = asyncio.new_event_loop()
|
|
78
|
-
thread = threading.Thread(target=run_event_loop, args=(loop,), daemon=True)
|
|
79
|
-
thread.start()
|
|
80
|
-
return loop
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def read_input(prompt: str) -> str:
|
|
84
|
-
sys.stdout.write(prompt)
|
|
85
|
-
sys.stdout.flush()
|
|
86
|
-
return sys.stdin.readline()
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def ask_for_quit_confirmation(program_name: str = "Exponent") -> bool:
|
|
90
|
-
while True:
|
|
91
|
-
try:
|
|
92
|
-
answer = (
|
|
93
|
-
input(f"Do you want to quit {program_name}? [y/\x1b[1mN\x1b[0m]")
|
|
94
|
-
.strip()
|
|
95
|
-
.lower()
|
|
96
|
-
)
|
|
97
|
-
if answer in {"y", "yes"}:
|
|
98
|
-
return True
|
|
99
|
-
elif answer in {"n", "no", ""}:
|
|
100
|
-
return False
|
|
101
|
-
except KeyboardInterrupt:
|
|
102
|
-
print()
|
|
103
|
-
return True
|
|
104
|
-
except EOFError:
|
|
105
|
-
print()
|
|
106
|
-
return True
|
|
107
|
-
|
|
108
|
-
|
|
109
71
|
class Spinner:
|
|
110
72
|
def __init__(self, text: str) -> None:
|
|
111
73
|
self.text = text
|
|
@@ -154,58 +116,6 @@ class Spinner:
|
|
|
154
116
|
sys.stdout.flush()
|
|
155
117
|
|
|
156
118
|
|
|
157
|
-
class ThinkingSpinner(Spinner):
|
|
158
|
-
def __init__(
|
|
159
|
-
self,
|
|
160
|
-
fg_color: tuple[int, int, int] | None = None,
|
|
161
|
-
text: str = "Exponent is thinking",
|
|
162
|
-
) -> None:
|
|
163
|
-
super().__init__(text)
|
|
164
|
-
self.fg_color = fg_color
|
|
165
|
-
self.bold = True
|
|
166
|
-
self.animation_chars = "⣾⣽⣻⢿⡿⣟⣯⣷" # More subtle animation
|
|
167
|
-
self.animation_speed = 8 # Medium speed animation
|
|
168
|
-
self.start_time = time.time() # Track when thinking started
|
|
169
|
-
|
|
170
|
-
def show(self) -> None:
|
|
171
|
-
if self.task is not None:
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
async def spinner(base_time: float) -> None:
|
|
175
|
-
color_start = ""
|
|
176
|
-
if self.fg_color:
|
|
177
|
-
if isinstance(self.fg_color, tuple) and len(self.fg_color) == 3:
|
|
178
|
-
r, g, b = self.fg_color
|
|
179
|
-
color_start = f"\x1b[38;2;{r};{g};{b}m"
|
|
180
|
-
elif isinstance(self.fg_color, int):
|
|
181
|
-
color_start = f"\x1b[{30 + self.fg_color}m"
|
|
182
|
-
|
|
183
|
-
bold_start = "\x1b[1m" if self.bold else ""
|
|
184
|
-
style_start = f"{color_start}{bold_start}"
|
|
185
|
-
style_end = "\x1b[0m"
|
|
186
|
-
|
|
187
|
-
while True:
|
|
188
|
-
t = time.time() - base_time
|
|
189
|
-
i = round(t * self.animation_speed) % len(self.animation_chars)
|
|
190
|
-
|
|
191
|
-
# Calculate elapsed seconds
|
|
192
|
-
elapsed = time.time() - self.start_time
|
|
193
|
-
if elapsed < 60:
|
|
194
|
-
timer = f"{int(elapsed)}s"
|
|
195
|
-
else:
|
|
196
|
-
mins = int(elapsed // 60)
|
|
197
|
-
secs = int(elapsed % 60)
|
|
198
|
-
timer = f"{mins}m {secs}s"
|
|
199
|
-
|
|
200
|
-
print(
|
|
201
|
-
f"\r{style_start}{self.animation_chars[i]} {self.text} ({timer}){style_end}",
|
|
202
|
-
end="",
|
|
203
|
-
)
|
|
204
|
-
await asyncio.sleep(0.1)
|
|
205
|
-
|
|
206
|
-
self.task = asyncio.get_event_loop().create_task(spinner(self.base_time))
|
|
207
|
-
|
|
208
|
-
|
|
209
119
|
class ConnectionTracker:
|
|
210
120
|
def __init__(self) -> None:
|
|
211
121
|
self.connected = True
|
|
@@ -152,7 +152,63 @@ class RemoteExecutionClient:
|
|
|
152
152
|
return None
|
|
153
153
|
|
|
154
154
|
data = json.dumps(msg_data["data"])
|
|
155
|
-
|
|
155
|
+
try:
|
|
156
|
+
request = msgspec.json.decode(data, type=CliRpcRequest)
|
|
157
|
+
except msgspec.DecodeError as e:
|
|
158
|
+
# Try and decode to get request_id if possible
|
|
159
|
+
request = msgspec.json.decode(data)
|
|
160
|
+
if isinstance(request, dict) and "request_id" in request:
|
|
161
|
+
request_id = request["request_id"]
|
|
162
|
+
if (
|
|
163
|
+
request.get("request", {}).get("type", {}) == "tool_execution"
|
|
164
|
+
) and (
|
|
165
|
+
"tool_input" in request["request"]
|
|
166
|
+
and "tool_name" in request["request"]["tool_input"]
|
|
167
|
+
):
|
|
168
|
+
tool_name = request["request"]["tool_input"]["tool_name"]
|
|
169
|
+
logger.error(
|
|
170
|
+
f"Error tool {tool_name} received in a request."
|
|
171
|
+
"Please ensure you are running the latest version of Indent. If this issue persists, please contact support."
|
|
172
|
+
)
|
|
173
|
+
await websocket.send(
|
|
174
|
+
json.dumps(
|
|
175
|
+
{
|
|
176
|
+
"type": "result",
|
|
177
|
+
"data": msgspec.to_builtins(
|
|
178
|
+
CliRpcResponse(
|
|
179
|
+
request_id=request_id,
|
|
180
|
+
response=ErrorResponse(
|
|
181
|
+
error_message=f"Unknown tool: {tool_name}. If you are running an older version of Indent, please upgrade to the latest version to ensure compatibility."
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
),
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
logger.error(
|
|
190
|
+
"Error decoding cli rpc request. Please ensure you are running the latest version of Indent."
|
|
191
|
+
)
|
|
192
|
+
await websocket.send(
|
|
193
|
+
json.dumps(
|
|
194
|
+
{
|
|
195
|
+
"type": "result",
|
|
196
|
+
"data": msgspec.to_builtins(
|
|
197
|
+
CliRpcResponse(
|
|
198
|
+
request_id=request_id,
|
|
199
|
+
response=ErrorResponse(
|
|
200
|
+
error_message=f"Unknown cli rpc request type: {request}",
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
),
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return None
|
|
209
|
+
else:
|
|
210
|
+
# If we couldn't get a request_id, re-raise and fail noisily
|
|
211
|
+
raise e
|
|
156
212
|
|
|
157
213
|
if isinstance(request.request, TerminateRequest):
|
|
158
214
|
await self.halt_all_code_executions()
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "indent"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
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"
|
|
@@ -163,3 +163,11 @@ max-returns = 10
|
|
|
163
163
|
|
|
164
164
|
[tool.ruff.format]
|
|
165
165
|
docstring-code-format = true
|
|
166
|
+
|
|
167
|
+
[tool.hatch.version]
|
|
168
|
+
source = "code"
|
|
169
|
+
path = "./version.py"
|
|
170
|
+
|
|
171
|
+
[tool.hatch.build.hooks.vcs]
|
|
172
|
+
version-file = "exponent/__init__.py"
|
|
173
|
+
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.8" # Keep in sync with pyproject.toml
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
import webbrowser
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from uuid import uuid4
|
|
8
|
-
|
|
9
|
-
import click
|
|
10
|
-
from git import GitCommandError, Repo
|
|
11
|
-
|
|
12
|
-
from exponent.commands.common import (
|
|
13
|
-
redirect_to_login,
|
|
14
|
-
verify_gh_app_installation,
|
|
15
|
-
)
|
|
16
|
-
from exponent.commands.settings import use_settings
|
|
17
|
-
from exponent.commands.types import exponent_cli_group
|
|
18
|
-
from exponent.core.config import Settings
|
|
19
|
-
from exponent.core.remote_execution.git import get_git_info
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@exponent_cli_group()
|
|
23
|
-
def github_app_cli() -> None:
|
|
24
|
-
"""Run AI-powered chat sessions."""
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@github_app_cli.command()
|
|
29
|
-
@use_settings
|
|
30
|
-
def install_github_app(
|
|
31
|
-
settings: Settings,
|
|
32
|
-
) -> None:
|
|
33
|
-
"""Start or reconnect to an Exponent session."""
|
|
34
|
-
if not settings.api_key:
|
|
35
|
-
redirect_to_login(settings)
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
loop = asyncio.get_event_loop()
|
|
39
|
-
|
|
40
|
-
api_key = settings.api_key
|
|
41
|
-
base_api_url = settings.get_base_api_url()
|
|
42
|
-
base_ws_url = settings.get_base_ws_url()
|
|
43
|
-
|
|
44
|
-
git_info = asyncio.run(get_git_info("."))
|
|
45
|
-
if not git_info:
|
|
46
|
-
raise RuntimeError("Not running inside of valid git repository")
|
|
47
|
-
|
|
48
|
-
install_url = "https://github.com/apps/indent/installations/new"
|
|
49
|
-
webbrowser.open(install_url)
|
|
50
|
-
|
|
51
|
-
click.confirm(
|
|
52
|
-
"Press enter once you've installed the github app.",
|
|
53
|
-
default=True,
|
|
54
|
-
abort=True,
|
|
55
|
-
prompt_suffix="",
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
click.secho("Verifying installation...", fg="yellow")
|
|
59
|
-
verified = loop.run_until_complete(
|
|
60
|
-
verify_gh_app_installation(api_key, base_api_url, base_ws_url, git_info)
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
if verified:
|
|
64
|
-
click.secho("Verified!", fg="green")
|
|
65
|
-
else:
|
|
66
|
-
click.secho("No verification found :(", fg="red")
|
|
67
|
-
sys.exit(1)
|
|
68
|
-
|
|
69
|
-
click.secho("Creating workflow file...", fg="yellow")
|
|
70
|
-
_create_workflow_yaml()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
WORKFLOW_YAML = """
|
|
74
|
-
name: Indent Action
|
|
75
|
-
|
|
76
|
-
on:
|
|
77
|
-
issue_comment:
|
|
78
|
-
types: [created]
|
|
79
|
-
pull_request_review_comment:
|
|
80
|
-
types: [created]
|
|
81
|
-
issues:
|
|
82
|
-
types: [opened, assigned]
|
|
83
|
-
pull_request_review:
|
|
84
|
-
types: [submitted]
|
|
85
|
-
|
|
86
|
-
jobs:
|
|
87
|
-
indent:
|
|
88
|
-
if: |
|
|
89
|
-
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@indent')) ||
|
|
90
|
-
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@indent')) ||
|
|
91
|
-
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@indent')) ||
|
|
92
|
-
(github.event_name == 'issues' && (contains(github.event.issue.body, '@indent') || contains(github.event.issue.title, '@indent')))
|
|
93
|
-
runs-on: ubuntu-latest
|
|
94
|
-
|
|
95
|
-
steps:
|
|
96
|
-
- name: Generate token for Indent app
|
|
97
|
-
id: generate_token
|
|
98
|
-
uses: actions/create-github-app-token@v1
|
|
99
|
-
with:
|
|
100
|
-
app-id: ${{ secrets.INDENT_APP_ID }}
|
|
101
|
-
private-key: ${{ secrets.INDENT_APP_PRIVATE_KEY }}
|
|
102
|
-
|
|
103
|
-
- name: Respond to mention
|
|
104
|
-
uses: actions/github-script@v7
|
|
105
|
-
with:
|
|
106
|
-
github-token: ${{ steps.generate_token.outputs.token }}
|
|
107
|
-
script: |
|
|
108
|
-
const issue_number = context.payload.issue?.number ||
|
|
109
|
-
context.payload.pull_request?.number ||
|
|
110
|
-
context.payload.review?.pull_request?.number;
|
|
111
|
-
|
|
112
|
-
await github.rest.issues.createComment({
|
|
113
|
-
owner: context.repo.owner,
|
|
114
|
-
repo: context.repo.repo,
|
|
115
|
-
issue_number: issue_number,
|
|
116
|
-
body: 'Hi it\'s me, Indent'
|
|
117
|
-
});
|
|
118
|
-
""".lstrip()
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _create_workflow_yaml() -> None:
|
|
122
|
-
git_branch = f"indent-workflow-{uuid4()}"
|
|
123
|
-
workflow_file = "indent-review.yml"
|
|
124
|
-
|
|
125
|
-
# 1. Locate the repository (searches upward until it finds .git).
|
|
126
|
-
repo = Repo(Path.cwd(), search_parent_directories=True)
|
|
127
|
-
if repo.bare or not repo.working_tree_dir:
|
|
128
|
-
sys.exit("Error: cannot operate inside a bare repository.")
|
|
129
|
-
|
|
130
|
-
original_branch = repo.active_branch.name if not repo.head.is_detached else None
|
|
131
|
-
|
|
132
|
-
# 2. Create or reuse the branch, then check it out.
|
|
133
|
-
try:
|
|
134
|
-
branch_ref = repo.create_head(git_branch)
|
|
135
|
-
except GitCommandError:
|
|
136
|
-
branch_ref = repo.heads[git_branch]
|
|
137
|
-
branch_ref.checkout()
|
|
138
|
-
|
|
139
|
-
# 3. Ensure workflow directory exists.
|
|
140
|
-
wf_dir = Path(repo.working_tree_dir) / ".github" / "workflows"
|
|
141
|
-
wf_dir.mkdir(parents=True, exist_ok=True)
|
|
142
|
-
|
|
143
|
-
yml = wf_dir / workflow_file
|
|
144
|
-
if not yml.exists():
|
|
145
|
-
yml.write_text(WORKFLOW_YAML)
|
|
146
|
-
# 5. Stage & commit.
|
|
147
|
-
repo.index.add([str(yml)])
|
|
148
|
-
repo.index.commit("Add Indent workflow template")
|
|
149
|
-
print(
|
|
150
|
-
f"Created {yml.relative_to(repo.working_tree_dir)} "
|
|
151
|
-
f"on branch '{git_branch}'."
|
|
152
|
-
)
|
|
153
|
-
else:
|
|
154
|
-
print(
|
|
155
|
-
f"{yml.relative_to(repo.working_tree_dir)} already exists; nothing to do."
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
subprocess.run(
|
|
159
|
-
["git", "push", "-u", "origin", git_branch],
|
|
160
|
-
cwd=repo.working_tree_dir,
|
|
161
|
-
check=True,
|
|
162
|
-
capture_output=False,
|
|
163
|
-
text=True,
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
pr_url: str | None = None
|
|
167
|
-
pr_title = f"Add Indent workflow ({workflow_file})"
|
|
168
|
-
pr_body = "This PR introduces an Indent github action workflow"
|
|
169
|
-
|
|
170
|
-
def run_gh(cmd: list[str]) -> subprocess.CompletedProcess[str]:
|
|
171
|
-
return subprocess.run(
|
|
172
|
-
["gh", *cmd],
|
|
173
|
-
cwd=repo.working_tree_dir,
|
|
174
|
-
check=True,
|
|
175
|
-
capture_output=True,
|
|
176
|
-
text=True,
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
try:
|
|
180
|
-
# Does a PR already exist for this head branch?
|
|
181
|
-
result = run_gh(["pr", "view", git_branch, "--json", "url"])
|
|
182
|
-
pr_url = json.loads(result.stdout)["url"]
|
|
183
|
-
except subprocess.CalledProcessError:
|
|
184
|
-
# No PR yet → create one
|
|
185
|
-
base = original_branch or repo.remotes.origin.refs[0].name.split("/")[-1]
|
|
186
|
-
run_gh(
|
|
187
|
-
[
|
|
188
|
-
"pr",
|
|
189
|
-
"create",
|
|
190
|
-
"--head",
|
|
191
|
-
git_branch,
|
|
192
|
-
"--base",
|
|
193
|
-
base,
|
|
194
|
-
"--title",
|
|
195
|
-
pr_title,
|
|
196
|
-
"--body",
|
|
197
|
-
pr_body,
|
|
198
|
-
]
|
|
199
|
-
)
|
|
200
|
-
# Fetch the newly created URL
|
|
201
|
-
result = run_gh(["pr", "view", git_branch, "--json", "url"])
|
|
202
|
-
pr_url = json.loads(result.stdout)["url"]
|
|
203
|
-
|
|
204
|
-
if pr_url:
|
|
205
|
-
click.secho(f"PR: {pr_url}", fg="green")
|
|
206
|
-
webbrowser.open(pr_url)
|
|
207
|
-
else:
|
|
208
|
-
click.secho("Failed to create PR!", fg="red")
|
|
209
|
-
|
|
210
|
-
if original_branch:
|
|
211
|
-
repo.git.checkout(original_branch)
|