ayechat-dev 0.36.9.20260201141756__py3-none-any.whl → 0.36.9.20260204011001__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.
- aye/controller/command_handlers.py +148 -29
- aye/controller/llm_handler.py +55 -3
- aye/controller/repl.py +9 -1
- aye/model/auth.py +21 -1
- aye/model/autodiff_config.py +32 -0
- aye/model/snapshot/__init__.py +98 -0
- aye/plugins/local_model.py +12 -3
- aye/presenter/repl_ui.py +6 -4
- {ayechat_dev-0.36.9.20260201141756.dist-info → ayechat_dev-0.36.9.20260204011001.dist-info}/METADATA +1 -1
- {ayechat_dev-0.36.9.20260201141756.dist-info → ayechat_dev-0.36.9.20260204011001.dist-info}/RECORD +14 -13
- {ayechat_dev-0.36.9.20260201141756.dist-info → ayechat_dev-0.36.9.20260204011001.dist-info}/WHEEL +0 -0
- {ayechat_dev-0.36.9.20260201141756.dist-info → ayechat_dev-0.36.9.20260204011001.dist-info}/entry_points.txt +0 -0
- {ayechat_dev-0.36.9.20260201141756.dist-info → ayechat_dev-0.36.9.20260204011001.dist-info}/licenses/LICENSE +0 -0
- {ayechat_dev-0.36.9.20260201141756.dist-info → ayechat_dev-0.36.9.20260204011001.dist-info}/top_level.txt +0 -0
|
@@ -7,7 +7,7 @@ from prompt_toolkit import PromptSession
|
|
|
7
7
|
from rich import print as rprint
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
|
|
10
|
-
from aye.model.auth import get_user_config, set_user_config
|
|
10
|
+
from aye.model.auth import get_user_config, set_user_config, delete_user_config
|
|
11
11
|
from aye.model.config import MODELS
|
|
12
12
|
from aye.presenter.repl_ui import print_error
|
|
13
13
|
from aye.controller.llm_invoker import invoke_llm
|
|
@@ -37,7 +37,7 @@ def handle_model_command(session: Optional[PromptSession], models: list, conf: A
|
|
|
37
37
|
num = int(tokens[1])
|
|
38
38
|
if 1 <= num <= len(models):
|
|
39
39
|
selected_id = models[num - 1]["id"]
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
# Check if this is an offline model and trigger download if needed
|
|
42
42
|
selected_model = models[num - 1]
|
|
43
43
|
if selected_model.get("type") == "offline":
|
|
@@ -49,7 +49,7 @@ def handle_model_command(session: Optional[PromptSession], models: list, conf: A
|
|
|
49
49
|
if download_response and not download_response.get("success", True):
|
|
50
50
|
rprint(f"[red]Failed to download model: {download_response.get('error', 'Unknown error')}[/]")
|
|
51
51
|
return
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
conf.selected_model = selected_id
|
|
54
54
|
set_user_config("selected_model", selected_id)
|
|
55
55
|
rprint(f"[green]Selected model: {models[num - 1]['name']}[/]")
|
|
@@ -81,7 +81,7 @@ def handle_model_command(session: Optional[PromptSession], models: list, conf: A
|
|
|
81
81
|
num = int(choice)
|
|
82
82
|
if 1 <= num <= len(models):
|
|
83
83
|
selected_id = models[num - 1]["id"]
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
# Check if this is an offline model and trigger download if needed
|
|
86
86
|
selected_model = models[num - 1]
|
|
87
87
|
if selected_model.get("type") == "offline":
|
|
@@ -93,7 +93,7 @@ def handle_model_command(session: Optional[PromptSession], models: list, conf: A
|
|
|
93
93
|
if download_response and not download_response.get("success", True):
|
|
94
94
|
rprint(f"[red]Failed to download model: {download_response.get('error', 'Unknown error')}[/]")
|
|
95
95
|
return
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
conf.selected_model = selected_id
|
|
98
98
|
set_user_config("selected_model", selected_id)
|
|
99
99
|
rprint(f"[green]Selected: {models[num - 1]['name']}[/]")
|
|
@@ -145,9 +145,28 @@ def handle_debug_command(tokens: list):
|
|
|
145
145
|
rprint(f"[yellow]Debug mode is {current.title()}[/]")
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
def handle_autodiff_command(tokens: list):
|
|
149
|
+
"""Handle the 'autodiff' command for toggling automatic diff display.
|
|
150
|
+
|
|
151
|
+
When autodiff is enabled, diffs are automatically displayed for every
|
|
152
|
+
file modified by an LLM response.
|
|
153
|
+
"""
|
|
154
|
+
if len(tokens) > 1:
|
|
155
|
+
val = tokens[1].lower()
|
|
156
|
+
if val in ("on", "off"):
|
|
157
|
+
set_user_config("autodiff", val)
|
|
158
|
+
rprint(f"[green]Autodiff set to {val.title()}[/]")
|
|
159
|
+
else:
|
|
160
|
+
rprint("[red]Usage: autodiff on|off[/]")
|
|
161
|
+
else:
|
|
162
|
+
current = get_user_config("autodiff", "off")
|
|
163
|
+
rprint(f"[yellow]Autodiff is {current.title()}[/]")
|
|
164
|
+
rprint("[dim]When on, diffs are shown automatically after each LLM file update.[/]")
|
|
165
|
+
|
|
166
|
+
|
|
148
167
|
def handle_completion_command(tokens: list) -> Optional[str]:
|
|
149
168
|
"""Handle the 'completion' command for switching completion styles.
|
|
150
|
-
|
|
169
|
+
|
|
151
170
|
Returns:
|
|
152
171
|
The new completion style if changed ('readline' or 'multi'), None otherwise.
|
|
153
172
|
"""
|
|
@@ -169,25 +188,125 @@ def handle_completion_command(tokens: list) -> Optional[str]:
|
|
|
169
188
|
return None
|
|
170
189
|
|
|
171
190
|
|
|
191
|
+
def handle_llm_command(session: Optional[PromptSession], tokens: list[str]) -> None:
|
|
192
|
+
"""Handle the 'llm' command for configuring OpenAI-compatible local model endpoint.
|
|
193
|
+
|
|
194
|
+
Usage:
|
|
195
|
+
llm - Interactively configure URL, key, and model
|
|
196
|
+
llm clear - Remove all LLM config values
|
|
197
|
+
|
|
198
|
+
Config keys stored in ~/.ayecfg:
|
|
199
|
+
llm_api_url
|
|
200
|
+
llm_api_key
|
|
201
|
+
llm_model
|
|
202
|
+
"""
|
|
203
|
+
# Handle 'llm clear' subcommand
|
|
204
|
+
if len(tokens) > 1 and tokens[1].lower() == "clear":
|
|
205
|
+
delete_user_config("llm_api_url")
|
|
206
|
+
delete_user_config("llm_api_key")
|
|
207
|
+
delete_user_config("llm_model")
|
|
208
|
+
rprint("[green]LLM config cleared.[/]")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
# Interactive configuration
|
|
212
|
+
current_url = get_user_config("llm_api_url", "")
|
|
213
|
+
current_key = get_user_config("llm_api_key", "")
|
|
214
|
+
current_model = get_user_config("llm_model", "")
|
|
215
|
+
|
|
216
|
+
# Show current status
|
|
217
|
+
rprint("\n[bold cyan]LLM Endpoint Configuration[/]")
|
|
218
|
+
rprint("[dim]Press Enter to keep current value, or type a new value.[/]\n")
|
|
219
|
+
|
|
220
|
+
if not session:
|
|
221
|
+
rprint("[red]Error: Interactive session not available.[/]")
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
# Prompt for URL (explicitly non-password; some prompt_toolkit versions may reuse app state)
|
|
226
|
+
url_display = current_url if current_url else "not set"
|
|
227
|
+
new_url = session.prompt(
|
|
228
|
+
f"LLM API URL (current: {url_display}): ",
|
|
229
|
+
is_password=False,
|
|
230
|
+
).strip()
|
|
231
|
+
final_url = new_url if new_url else current_url
|
|
232
|
+
|
|
233
|
+
# Prompt for API key (hidden input)
|
|
234
|
+
key_display = "set" if current_key else "not set"
|
|
235
|
+
new_key = session.prompt(
|
|
236
|
+
f"LLM API KEY (current: {key_display}): ",
|
|
237
|
+
is_password=True,
|
|
238
|
+
).strip()
|
|
239
|
+
final_key = new_key if new_key else current_key
|
|
240
|
+
|
|
241
|
+
# Prompt for model (explicitly non-password)
|
|
242
|
+
model_display = current_model if current_model else "not set"
|
|
243
|
+
new_model = session.prompt(
|
|
244
|
+
f"LLM MODEL (current: {model_display}): ",
|
|
245
|
+
is_password=False,
|
|
246
|
+
).strip()
|
|
247
|
+
final_model = new_model if new_model else current_model
|
|
248
|
+
|
|
249
|
+
except (EOFError, KeyboardInterrupt):
|
|
250
|
+
rprint("\n[yellow]Configuration cancelled.[/]")
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# Save values (only if they have content)
|
|
254
|
+
if final_url:
|
|
255
|
+
set_user_config("llm_api_url", final_url)
|
|
256
|
+
elif current_url and not new_url:
|
|
257
|
+
# Keep existing
|
|
258
|
+
pass
|
|
259
|
+
else:
|
|
260
|
+
delete_user_config("llm_api_url")
|
|
261
|
+
|
|
262
|
+
if final_key:
|
|
263
|
+
set_user_config("llm_api_key", final_key)
|
|
264
|
+
elif current_key and not new_key:
|
|
265
|
+
# Keep existing
|
|
266
|
+
pass
|
|
267
|
+
else:
|
|
268
|
+
delete_user_config("llm_api_key")
|
|
269
|
+
|
|
270
|
+
if final_model:
|
|
271
|
+
set_user_config("llm_model", final_model)
|
|
272
|
+
elif current_model and not new_model:
|
|
273
|
+
# Keep existing
|
|
274
|
+
pass
|
|
275
|
+
else:
|
|
276
|
+
delete_user_config("llm_model")
|
|
277
|
+
|
|
278
|
+
# Print confirmation
|
|
279
|
+
rprint("\n[bold cyan]LLM Configuration Updated[/]")
|
|
280
|
+
rprint(f" URL: {final_url if final_url else '[dim]not set[/]'}")
|
|
281
|
+
rprint(f" KEY: {'[dim]set (hidden)[/]' if final_key else '[dim]not set[/]'}")
|
|
282
|
+
rprint(f" MODEL: {final_model if final_model else '[dim]not set[/]'}")
|
|
283
|
+
|
|
284
|
+
# Show status message
|
|
285
|
+
if final_url and final_key:
|
|
286
|
+
rprint("\n[green] OpenAI-compatible endpoint is configured and active.[/]")
|
|
287
|
+
else:
|
|
288
|
+
rprint("\n[yellow] Both URL and KEY are required for the local LLM endpoint to be active.[/]")
|
|
289
|
+
|
|
290
|
+
|
|
172
291
|
def _expand_file_patterns(patterns: list[str], conf: Any) -> list[str]:
|
|
173
292
|
"""Expand wildcard patterns and return a list of existing file paths."""
|
|
174
293
|
expanded_files = []
|
|
175
|
-
|
|
294
|
+
|
|
176
295
|
for pattern in patterns:
|
|
177
296
|
pattern = pattern.strip()
|
|
178
297
|
if not pattern:
|
|
179
298
|
continue
|
|
180
|
-
|
|
299
|
+
|
|
181
300
|
# Check if it's a direct file path first
|
|
182
301
|
direct_path = conf.root / pattern
|
|
183
302
|
if direct_path.is_file():
|
|
184
303
|
expanded_files.append(pattern)
|
|
185
304
|
continue
|
|
186
|
-
|
|
305
|
+
|
|
187
306
|
# Use glob to expand wildcards
|
|
188
307
|
# Search relative to the project root
|
|
189
308
|
matched_paths = list(conf.root.glob(pattern))
|
|
190
|
-
|
|
309
|
+
|
|
191
310
|
# Add relative paths of matched files
|
|
192
311
|
for matched_path in matched_paths:
|
|
193
312
|
if matched_path.is_file():
|
|
@@ -197,26 +316,26 @@ def _expand_file_patterns(patterns: list[str], conf: Any) -> list[str]:
|
|
|
197
316
|
except ValueError:
|
|
198
317
|
# If we can't make it relative, use the original pattern
|
|
199
318
|
expanded_files.append(pattern)
|
|
200
|
-
|
|
319
|
+
|
|
201
320
|
return expanded_files
|
|
202
321
|
|
|
203
322
|
|
|
204
323
|
def handle_with_command(
|
|
205
|
-
prompt: str,
|
|
206
|
-
conf: Any,
|
|
207
|
-
console: Console,
|
|
208
|
-
chat_id: int,
|
|
324
|
+
prompt: str,
|
|
325
|
+
conf: Any,
|
|
326
|
+
console: Console,
|
|
327
|
+
chat_id: int,
|
|
209
328
|
chat_id_file: Path
|
|
210
329
|
) -> Optional[int]:
|
|
211
330
|
"""Handle the 'with' command for file-specific prompts with wildcard support.
|
|
212
|
-
|
|
331
|
+
|
|
213
332
|
Args:
|
|
214
333
|
prompt: The full prompt string starting with 'with'
|
|
215
334
|
conf: Configuration object
|
|
216
335
|
console: Rich console for output
|
|
217
336
|
chat_id: Current chat ID
|
|
218
337
|
chat_id_file: Path to chat ID file
|
|
219
|
-
|
|
338
|
+
|
|
220
339
|
Returns:
|
|
221
340
|
New chat_id if available, None otherwise
|
|
222
341
|
"""
|
|
@@ -234,16 +353,16 @@ def handle_with_command(
|
|
|
234
353
|
|
|
235
354
|
# Parse file patterns (can include wildcards)
|
|
236
355
|
file_patterns = [f.strip() for f in file_list_str.replace(",", " ").split() if f.strip()]
|
|
237
|
-
|
|
356
|
+
|
|
238
357
|
# Expand wildcards to get actual file paths
|
|
239
358
|
expanded_files = _expand_file_patterns(file_patterns, conf)
|
|
240
|
-
|
|
359
|
+
|
|
241
360
|
if not expanded_files:
|
|
242
361
|
rprint("[red]Error: No files found matching the specified patterns.[/red]")
|
|
243
362
|
return None
|
|
244
|
-
|
|
363
|
+
|
|
245
364
|
explicit_source_files = {}
|
|
246
|
-
|
|
365
|
+
|
|
247
366
|
for file_name in expanded_files:
|
|
248
367
|
file_path = conf.root / file_name
|
|
249
368
|
if not file_path.is_file():
|
|
@@ -254,11 +373,11 @@ def handle_with_command(
|
|
|
254
373
|
except Exception as e:
|
|
255
374
|
rprint(f"[red]Could not read file '{file_name}': {e}[/red]")
|
|
256
375
|
continue # Continue with other files instead of breaking
|
|
257
|
-
|
|
376
|
+
|
|
258
377
|
if not explicit_source_files:
|
|
259
378
|
rprint("[red]Error: No readable files found.[/red]")
|
|
260
379
|
return None
|
|
261
|
-
|
|
380
|
+
|
|
262
381
|
# Show which files were included
|
|
263
382
|
if conf.verbose or len(explicit_source_files) != len(expanded_files):
|
|
264
383
|
rprint(f"[cyan]Including {len(explicit_source_files)} file(s): {', '.join(explicit_source_files.keys())}[/cyan]")
|
|
@@ -272,20 +391,20 @@ def handle_with_command(
|
|
|
272
391
|
verbose=conf.verbose,
|
|
273
392
|
explicit_source_files=explicit_source_files
|
|
274
393
|
)
|
|
275
|
-
|
|
394
|
+
|
|
276
395
|
if llm_response:
|
|
277
396
|
new_chat_id = process_llm_response(
|
|
278
|
-
response=llm_response,
|
|
279
|
-
conf=conf,
|
|
280
|
-
console=console,
|
|
281
|
-
prompt=new_prompt_str.strip(),
|
|
397
|
+
response=llm_response,
|
|
398
|
+
conf=conf,
|
|
399
|
+
console=console,
|
|
400
|
+
prompt=new_prompt_str.strip(),
|
|
282
401
|
chat_id_file=chat_id_file if llm_response.chat_id else None
|
|
283
402
|
)
|
|
284
403
|
return new_chat_id
|
|
285
404
|
else:
|
|
286
405
|
rprint("[yellow]No response from LLM.[/]")
|
|
287
406
|
return None
|
|
288
|
-
|
|
407
|
+
|
|
289
408
|
except Exception as exc:
|
|
290
409
|
handle_llm_error(exc)
|
|
291
410
|
return None
|
aye/controller/llm_handler.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import Any, Optional
|
|
2
|
+
from typing import Any, Optional, List
|
|
3
3
|
|
|
4
4
|
from rich import print as rprint
|
|
5
5
|
from rich.console import Console
|
|
@@ -11,10 +11,12 @@ from aye.presenter.repl_ui import (
|
|
|
11
11
|
print_files_updated,
|
|
12
12
|
print_error
|
|
13
13
|
)
|
|
14
|
-
from aye.
|
|
14
|
+
from aye.presenter import diff_presenter
|
|
15
|
+
from aye.model.snapshot import apply_updates, get_diff_base_for_file
|
|
15
16
|
from aye.model.file_processor import filter_unchanged_files, make_paths_relative
|
|
16
17
|
from aye.model.models import LLMResponse
|
|
17
18
|
from aye.model.auth import get_user_config
|
|
19
|
+
from aye.model.autodiff_config import is_autodiff_enabled
|
|
18
20
|
from aye.model.write_validator import (
|
|
19
21
|
check_files_against_ignore_patterns,
|
|
20
22
|
is_strict_mode_enabled,
|
|
@@ -52,6 +54,51 @@ def _maybe_print_restore_tip(conf: Any, console: Console) -> None:
|
|
|
52
54
|
console.print(Padding(msg, (0, 4, 0, 4)))
|
|
53
55
|
|
|
54
56
|
|
|
57
|
+
def _run_autodiff(updated_files: List[dict], batch_id: str, conf: Any, console: Console) -> None:
|
|
58
|
+
"""Display diffs for all updated files against their snapshot versions.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
updated_files: List of file dicts with 'file_name' keys
|
|
62
|
+
batch_id: The batch identifier from apply_updates()
|
|
63
|
+
conf: Configuration object with root path
|
|
64
|
+
console: Rich console for output
|
|
65
|
+
"""
|
|
66
|
+
verbose = getattr(conf, 'verbose', False)
|
|
67
|
+
debug = get_user_config("debug", "off").lower() == "on"
|
|
68
|
+
|
|
69
|
+
console.print(Padding("[dim]───── Auto-diff (autodiff=on) ─────[/]", (1, 0, 0, 0)))
|
|
70
|
+
|
|
71
|
+
for item in updated_files:
|
|
72
|
+
file_name = item.get("file_name")
|
|
73
|
+
if not file_name:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
file_path = Path(file_name)
|
|
77
|
+
|
|
78
|
+
# Get the snapshot reference for this file
|
|
79
|
+
diff_base = get_diff_base_for_file(batch_id, file_path)
|
|
80
|
+
|
|
81
|
+
if diff_base is None:
|
|
82
|
+
if verbose or debug:
|
|
83
|
+
rprint(f"[yellow]Warning: Could not find snapshot for {file_name}, skipping autodiff[/]")
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
snapshot_ref, is_git_ref = diff_base
|
|
87
|
+
|
|
88
|
+
# Print file header
|
|
89
|
+
console.print(f"\n[bold cyan]{file_name}[/]")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# show_diff expects: (current_file, snapshot_ref, is_stash_ref)
|
|
93
|
+
# For autodiff, we diff the current (new) file against the snapshot (old)
|
|
94
|
+
diff_presenter.show_diff(file_path, snapshot_ref, is_stash_ref=is_git_ref)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
if verbose or debug:
|
|
97
|
+
rprint(f"[yellow]Warning: Could not show diff for {file_name}: {e}[/]")
|
|
98
|
+
|
|
99
|
+
console.print(Padding("[dim]───── End auto-diff ─────[/]", (1, 0, 0, 0)))
|
|
100
|
+
|
|
101
|
+
|
|
55
102
|
def process_llm_response(
|
|
56
103
|
response: LLMResponse,
|
|
57
104
|
conf: Any,
|
|
@@ -118,12 +165,17 @@ def process_llm_response(
|
|
|
118
165
|
else:
|
|
119
166
|
# Apply updates to the model (Model update)
|
|
120
167
|
try:
|
|
121
|
-
apply_updates(updated_files, prompt)
|
|
168
|
+
batch_id = apply_updates(updated_files, prompt)
|
|
122
169
|
file_names = [item.get("file_name") for item in updated_files if "file_name" in item]
|
|
123
170
|
if file_names:
|
|
124
171
|
# Update the view
|
|
125
172
|
print_files_updated(console, file_names)
|
|
126
173
|
_maybe_print_restore_tip(conf, console)
|
|
174
|
+
|
|
175
|
+
# Run autodiff if enabled
|
|
176
|
+
if is_autodiff_enabled():
|
|
177
|
+
_run_autodiff(updated_files, batch_id, conf, console)
|
|
178
|
+
|
|
127
179
|
except Exception as e:
|
|
128
180
|
rprint(f"[red]Error applying updates:[/] {e}")
|
|
129
181
|
|
aye/controller/repl.py
CHANGED
|
@@ -41,6 +41,8 @@ from aye.controller.command_handlers import (
|
|
|
41
41
|
handle_completion_command,
|
|
42
42
|
handle_with_command,
|
|
43
43
|
handle_blog_command,
|
|
44
|
+
handle_llm_command,
|
|
45
|
+
handle_autodiff_command,
|
|
44
46
|
)
|
|
45
47
|
|
|
46
48
|
DEBUG = False
|
|
@@ -284,7 +286,7 @@ def _execute_forced_shell_command(command: str, args: List[str], conf: Any) -> N
|
|
|
284
286
|
def chat_repl(conf: Any) -> None:
|
|
285
287
|
is_first_run = run_first_time_tutorial_if_needed()
|
|
286
288
|
|
|
287
|
-
BUILTIN_COMMANDS = ["with", "blog", "new", "history", "diff", "restore", "undo", "keep", "model", "verbose", "debug", "completion", "exit", "quit", ":q", "help", "cd", "db"]
|
|
289
|
+
BUILTIN_COMMANDS = ["with", "blog", "new", "history", "diff", "restore", "undo", "keep", "model", "verbose", "debug", "autodiff", "completion", "exit", "quit", ":q", "help", "cd", "db", "llm"]
|
|
288
290
|
|
|
289
291
|
# Get the completion style setting
|
|
290
292
|
completion_style = get_user_config("completion_style", "readline").lower()
|
|
@@ -415,6 +417,9 @@ def chat_repl(conf: Any) -> None:
|
|
|
415
417
|
elif lowered_first == "debug":
|
|
416
418
|
telemetry.record_command("debug", has_args=len(tokens) > 1, prefix=_AYE_PREFIX)
|
|
417
419
|
handle_debug_command(tokens)
|
|
420
|
+
elif lowered_first == "autodiff":
|
|
421
|
+
telemetry.record_command("autodiff", has_args=len(tokens) > 1, prefix=_AYE_PREFIX)
|
|
422
|
+
handle_autodiff_command(tokens)
|
|
418
423
|
elif lowered_first == "completion":
|
|
419
424
|
telemetry.record_command("completion", has_args=len(tokens) > 1, prefix=_AYE_PREFIX)
|
|
420
425
|
new_style = handle_completion_command(tokens)
|
|
@@ -429,6 +434,9 @@ def chat_repl(conf: Any) -> None:
|
|
|
429
434
|
# Recreate the session with the new completer
|
|
430
435
|
session = create_prompt_session(completer, new_style)
|
|
431
436
|
rprint(f"[green]Completion style is now active.[/]")
|
|
437
|
+
elif lowered_first == "llm":
|
|
438
|
+
telemetry.record_command("llm", has_args=len(tokens) > 1, prefix=_AYE_PREFIX)
|
|
439
|
+
handle_llm_command(session, tokens)
|
|
432
440
|
elif lowered_first == "blog":
|
|
433
441
|
telemetry.record_command("blog", has_args=len(tokens) > 1, prefix=_AYE_PREFIX)
|
|
434
442
|
telemetry.record_llm_prompt("LLM <blog>")
|
aye/model/auth.py
CHANGED
|
@@ -58,6 +58,27 @@ def set_user_config(key: str, value: Any) -> None:
|
|
|
58
58
|
TOKEN_FILE.chmod(0o600)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def delete_user_config(key: str) -> None:
|
|
62
|
+
"""Delete a user config key from the [default] section.
|
|
63
|
+
|
|
64
|
+
If the key doesn't exist, this is a no-op.
|
|
65
|
+
Preserves other settings and maintains file permissions.
|
|
66
|
+
"""
|
|
67
|
+
config = _parse_user_config()
|
|
68
|
+
if key not in config:
|
|
69
|
+
return
|
|
70
|
+
config.pop(key, None)
|
|
71
|
+
if not config:
|
|
72
|
+
# If no config left, remove the file entirely
|
|
73
|
+
TOKEN_FILE.unlink(missing_ok=True)
|
|
74
|
+
else:
|
|
75
|
+
new_content = "[default]\n"
|
|
76
|
+
for k, v in config.items():
|
|
77
|
+
new_content += f"{k}={v}\n"
|
|
78
|
+
TOKEN_FILE.write_text(new_content, encoding="utf-8")
|
|
79
|
+
TOKEN_FILE.chmod(0o600)
|
|
80
|
+
|
|
81
|
+
|
|
61
82
|
def store_token(token: str) -> None:
|
|
62
83
|
"""Persist the token in ~/.ayecfg or value from AYE_TOKEN_FILE environment variable (unless AYE_TOKEN is set)."""
|
|
63
84
|
token = token.strip()
|
|
@@ -122,4 +143,3 @@ def login_flow() -> None:
|
|
|
122
143
|
token = typer.prompt("Paste your token", hide_input=True)
|
|
123
144
|
store_token(token.strip())
|
|
124
145
|
typer.secho("✅ Token saved.", fg=typer.colors.GREEN)
|
|
125
|
-
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Autodiff configuration for automatic diff display after LLM changes.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to check if autodiff mode is enabled.
|
|
4
|
+
When enabled, diffs are automatically displayed for every file modified
|
|
5
|
+
by an LLM response.
|
|
6
|
+
|
|
7
|
+
See: autodiff.md for the full design plan.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from aye.model.auth import get_user_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Config key for autodiff mode
|
|
14
|
+
AUTODIFF_KEY = "autodiff"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_autodiff_enabled() -> bool:
|
|
18
|
+
"""Check if autodiff mode is enabled.
|
|
19
|
+
|
|
20
|
+
When enabled, diffs are automatically displayed for every file
|
|
21
|
+
modified by an LLM response, immediately after the optimistic
|
|
22
|
+
write is applied.
|
|
23
|
+
|
|
24
|
+
Can be set via:
|
|
25
|
+
- Environment variable: AYE_AUTODIFF=on
|
|
26
|
+
- Config file (~/.ayecfg): autodiff=on
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
True if autodiff mode is enabled, False otherwise (default)
|
|
30
|
+
"""
|
|
31
|
+
value = get_user_config(AUTODIFF_KEY, "off")
|
|
32
|
+
return str(value).lower() in ("on", "true", "1", "yes")
|
aye/model/snapshot/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ Note:
|
|
|
7
7
|
- Git stash snapshots (GitStashBackend) are intentionally NOT used.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import json
|
|
10
11
|
import subprocess
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Dict, List, Optional, Tuple, Union
|
|
@@ -32,6 +33,7 @@ __all__ = [
|
|
|
32
33
|
"delete_snapshot",
|
|
33
34
|
"prune_snapshots",
|
|
34
35
|
"cleanup_snapshots",
|
|
36
|
+
"get_diff_base_for_file",
|
|
35
37
|
# Utilities
|
|
36
38
|
"get_backend",
|
|
37
39
|
"reset_backend",
|
|
@@ -145,6 +147,102 @@ def cleanup_snapshots(older_than_days: int = 30) -> int:
|
|
|
145
147
|
return get_backend().cleanup_snapshots(older_than_days)
|
|
146
148
|
|
|
147
149
|
|
|
150
|
+
def get_diff_base_for_file(batch_id: str, file_path: Path) -> Optional[Tuple[str, bool]]:
|
|
151
|
+
"""Return the snapshot reference for a file in a given batch.
|
|
152
|
+
|
|
153
|
+
This provides a backend-agnostic way to get a reference suitable for
|
|
154
|
+
diff_presenter.show_diff().
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
batch_id: The batch identifier returned by apply_updates()
|
|
158
|
+
file_path: The file path to get the snapshot reference for
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Tuple of (snapshot_ref, is_git_ref) where:
|
|
162
|
+
- snapshot_ref: For FileBasedBackend, a filesystem path to the snapshot file.
|
|
163
|
+
For GitRefBackend, a 'refname:repo_rel_path' string.
|
|
164
|
+
- is_git_ref: True if snapshot_ref is a git ref format, False for filesystem path.
|
|
165
|
+
Returns None if the file is not found in the snapshot.
|
|
166
|
+
"""
|
|
167
|
+
backend = get_backend()
|
|
168
|
+
|
|
169
|
+
if isinstance(backend, FileBasedBackend):
|
|
170
|
+
return _get_diff_base_file_backend(backend, batch_id, file_path)
|
|
171
|
+
elif isinstance(backend, GitRefBackend):
|
|
172
|
+
return _get_diff_base_git_backend(backend, batch_id, file_path)
|
|
173
|
+
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _get_diff_base_file_backend(
|
|
178
|
+
backend: FileBasedBackend, batch_id: str, file_path: Path
|
|
179
|
+
) -> Optional[Tuple[str, bool]]:
|
|
180
|
+
"""Get diff base for FileBasedBackend.
|
|
181
|
+
|
|
182
|
+
Reads the metadata.json from the snapshot batch directory to find
|
|
183
|
+
the snapshot path for the given file.
|
|
184
|
+
"""
|
|
185
|
+
# Find the batch directory matching the batch_id
|
|
186
|
+
# batch_id format is like "001_20231201T120000"
|
|
187
|
+
batch_dir = None
|
|
188
|
+
if backend.snap_root.is_dir():
|
|
189
|
+
for dir_path in backend.snap_root.iterdir():
|
|
190
|
+
if dir_path.is_dir() and dir_path.name == batch_id:
|
|
191
|
+
batch_dir = dir_path
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
if batch_dir is None:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
meta_file = batch_dir / "metadata.json"
|
|
198
|
+
if not meta_file.is_file():
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
203
|
+
except (json.JSONDecodeError, OSError):
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
# Resolve the target file path for comparison
|
|
207
|
+
file_resolved = file_path.resolve()
|
|
208
|
+
|
|
209
|
+
# Find the matching entry in metadata
|
|
210
|
+
for entry in meta.get("files", []):
|
|
211
|
+
original_path = entry.get("original")
|
|
212
|
+
snapshot_path = entry.get("snapshot")
|
|
213
|
+
|
|
214
|
+
if not original_path or not snapshot_path:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
if Path(original_path).resolve() == file_resolved:
|
|
218
|
+
# Return the snapshot path as a string, not a git ref
|
|
219
|
+
return (snapshot_path, False)
|
|
220
|
+
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _get_diff_base_git_backend(
|
|
225
|
+
backend: GitRefBackend, batch_id: str, file_path: Path
|
|
226
|
+
) -> Optional[Tuple[str, bool]]:
|
|
227
|
+
"""Get diff base for GitRefBackend.
|
|
228
|
+
|
|
229
|
+
Returns a ref:path format suitable for git show.
|
|
230
|
+
"""
|
|
231
|
+
# Construct the refname from batch_id
|
|
232
|
+
refname = f"{backend.REF_NAMESPACE}/{batch_id}"
|
|
233
|
+
|
|
234
|
+
# Get repo-relative path
|
|
235
|
+
try:
|
|
236
|
+
file_resolved = file_path.resolve()
|
|
237
|
+
rel_path = file_resolved.relative_to(backend.git_root).as_posix()
|
|
238
|
+
except ValueError:
|
|
239
|
+
# File is outside git root
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
# Return the git ref format
|
|
243
|
+
return (f"{refname}:{rel_path}", True)
|
|
244
|
+
|
|
245
|
+
|
|
148
246
|
# ------------------------------------------------------------------
|
|
149
247
|
# Legacy helper functions (for backward compatibility with tests)
|
|
150
248
|
# ------------------------------------------------------------------
|
aye/plugins/local_model.py
CHANGED
|
@@ -8,6 +8,7 @@ from rich import print as rprint
|
|
|
8
8
|
|
|
9
9
|
from .plugin_base import Plugin
|
|
10
10
|
from aye.model.config import SYSTEM_PROMPT, MODELS, DEFAULT_MAX_OUTPUT_TOKENS
|
|
11
|
+
from aye.model.auth import get_user_config
|
|
11
12
|
from aye.controller.util import is_truncated_json
|
|
12
13
|
|
|
13
14
|
LLM_TIMEOUT = 600.0
|
|
@@ -179,9 +180,17 @@ class LocalModelPlugin(Plugin):
|
|
|
179
180
|
return self._create_error_response(f"Error calling Databricks API: {str(e)}")
|
|
180
181
|
|
|
181
182
|
def _handle_openai_compatible(self, prompt: str, source_files: Dict[str, str], chat_id: Optional[int] = None, system_prompt: Optional[str] = None, max_output_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS) -> Optional[Dict[str, Any]]:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
"""Handle OpenAI-compatible API endpoints.
|
|
184
|
+
|
|
185
|
+
Reads configuration from:
|
|
186
|
+
- get_user_config("llm_api_url") / AYE_LLM_API_URL
|
|
187
|
+
- get_user_config("llm_api_key") / AYE_LLM_API_KEY
|
|
188
|
+
- get_user_config("llm_model") / AYE_LLM_MODEL (default: gpt-3.5-turbo)
|
|
189
|
+
"""
|
|
190
|
+
# Read from config (supports both ~/.ayecfg and AYE_LLM_* env vars)
|
|
191
|
+
api_url = get_user_config("llm_api_url")
|
|
192
|
+
api_key = get_user_config("llm_api_key")
|
|
193
|
+
model_name = get_user_config("llm_model", "gpt-3.5-turbo")
|
|
185
194
|
|
|
186
195
|
if not api_url or not api_key:
|
|
187
196
|
return None
|
aye/presenter/repl_ui.py
CHANGED
|
@@ -88,13 +88,15 @@ def print_help_message():
|
|
|
88
88
|
# Some commands are intentionally undocumented: keep them as such.
|
|
89
89
|
("@filename", "Include a file in your prompt inline (e.g., \"explain @main.py\"). Supports wildcards (e.g., @*.py, @src/*.js)."),
|
|
90
90
|
("!command", "Force shell execution (e.g., \"!echo hello\")."),
|
|
91
|
-
("model", "Select a different model. Selection will persist between sessions."),
|
|
92
|
-
(r"verbose \[on|off]", "Toggle verbose mode to increase or decrease chattiness (on/off, persists between sessions)"),
|
|
93
|
-
(r"completion \[readline|multi]", "Switch auto-completion style (readline or multi, persists between sessions)"),
|
|
94
91
|
("new", "Start a new chat session (if you want to change the subject)"),
|
|
92
|
+
(r"restore, undo \[id] \[file]", "Revert changes to the last state, a specific snapshot `id`, or for a single `file`."),
|
|
95
93
|
("history", "Show snapshot history"),
|
|
96
94
|
(r"diff <file> \[snapshot_id]", "Show diff of file with the latest snapshot, or a specified snapshot"),
|
|
97
|
-
(
|
|
95
|
+
("model", "Select a different model. Selection will persist between sessions."),
|
|
96
|
+
(r"autodiff \[on|off]", "Toggle automatic diff display after LLM file updates (off by default, persists between sessions)"),
|
|
97
|
+
("llm", "Configure OpenAI-compatible LLM endpoint (URL, key, model). Use 'llm clear' to reset."),
|
|
98
|
+
(r"verbose \[on|off]", "Toggle verbose mode to increase or decrease chattiness (on/off, persists between sessions)"),
|
|
99
|
+
(r"completion \[readline|multi]", "Switch auto-completion style (readline or multi, persists between sessions)"),
|
|
98
100
|
("keep [N]", "Keep only N most recent snapshots (10 by default)"),
|
|
99
101
|
("exit, quit, Ctrl+D", "Exit the chat session"),
|
|
100
102
|
("help", "Show this help message"),
|
{ayechat_dev-0.36.9.20260201141756.dist-info → ayechat_dev-0.36.9.20260204011001.dist-info}/RECORD
RENAMED
|
@@ -3,18 +3,19 @@ aye/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
3
3
|
aye/__main__.py,sha256=fj7pl0i_mLvpKAbpKtotU3zboLBTivsILSLKuH5M5Sg,5377
|
|
4
4
|
aye/__main_chat__.py,sha256=R6RaidxG3Px5TaYxcoWAuIleE5KUZlceneUB6u_9UVU,1066
|
|
5
5
|
aye/controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
aye/controller/command_handlers.py,sha256=
|
|
6
|
+
aye/controller/command_handlers.py,sha256=S_HOYY91lbLkGtCkby28T8qUFPNtfy-CbOGn9VGZtTo,17446
|
|
7
7
|
aye/controller/commands.py,sha256=sXmK_sgNBrw9Fs7mKcr93-wsu740ZlvWSisQfS-1EUE,12278
|
|
8
|
-
aye/controller/llm_handler.py,sha256=
|
|
8
|
+
aye/controller/llm_handler.py,sha256=gY3X2rHcvhPp8iqp_Vor7QpbwHltmcm_Uu9MWF1Z598,7120
|
|
9
9
|
aye/controller/llm_invoker.py,sha256=p_Vk2a3YrWKwDupLfSVRinR5llDfq1Fb_f7WrYozK6M,14127
|
|
10
10
|
aye/controller/plugin_manager.py,sha256=9ZuITyA5sQJJJU-IntLQ1SsxXsDnbgZKPOF4e9VmsEU,3018
|
|
11
|
-
aye/controller/repl.py,sha256=
|
|
11
|
+
aye/controller/repl.py,sha256=nSlzAHD8MQlQ7vnpBTLVzA1jQ8jkNjxUnUoqE31chxY,27028
|
|
12
12
|
aye/controller/tutorial.py,sha256=lc92jOcJOYCVrrjTEF0Suk4-8jn-ku98kTJEIL8taUA,7254
|
|
13
13
|
aye/controller/util.py,sha256=gBmktDEaY63OKhgzZHA2IFrgcWUN_Iphn3e1daEeUBI,2828
|
|
14
14
|
aye/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
aye/model/api.py,sha256=HhSMQQ_szdC2ZPOSfNsJRbs1FRwb6WyYIeLejB2ScbA,13272
|
|
16
16
|
aye/model/ast_chunker.py,sha256=rVcDdynVUXXyxWVgtUcsee_STqB7SAwP776ktWTlYig,4462
|
|
17
|
-
aye/model/auth.py,sha256=
|
|
17
|
+
aye/model/auth.py,sha256=ozV_uQxdqXtUoWO3nZwpzVnDOIfnRAmSMC6W0N724vE,4800
|
|
18
|
+
aye/model/autodiff_config.py,sha256=b8pyudkJFYXF5JWPxft0bH5uazeCab9i-A11dNt1d7U,931
|
|
18
19
|
aye/model/config.py,sha256=o6bQhj5gqhSqtWD6DLow7NNy6Hdaede02h_xb7uPLXo,9280
|
|
19
20
|
aye/model/download_plugins.py,sha256=6omyFGdxlEIb7tKPLq4rRVrRYeCPUUCE8aZHvJAKGSc,4442
|
|
20
21
|
aye/model/file_processor.py,sha256=b7YGvHAmhGto9JbtzcfrsdkFtksHbosYt-42EnR22Uo,2131
|
|
@@ -34,7 +35,7 @@ aye/model/index_manager/index_manager_executor.py,sha256=rSI8OX9bFld5ta-ajfT8eWU
|
|
|
34
35
|
aye/model/index_manager/index_manager_file_ops.py,sha256=petgGU0ZtXOvLkHAQuzSbzqNIew5HygBOAy4yG6rTlk,7656
|
|
35
36
|
aye/model/index_manager/index_manager_state.py,sha256=LflyC8UbE8fT7Tyag1ueh7TBisPa15cRhs0JI1-jVEw,17587
|
|
36
37
|
aye/model/index_manager/index_manager_utils.py,sha256=B4NCYuScxQcG46WoUEx8mxfWKDjTlrSf3coRQCydiBM,4615
|
|
37
|
-
aye/model/snapshot/__init__.py,sha256=
|
|
38
|
+
aye/model/snapshot/__init__.py,sha256=XyEaYF0WS0wuGZI9t2vcA1UUxWbwj5CEkwKFYUSQwKc,10152
|
|
38
39
|
aye/model/snapshot/base.py,sha256=5fzxM_85avxHwocv-w6PwlxDPzvMdTbuoUE_9P6D844,2009
|
|
39
40
|
aye/model/snapshot/file_backend.py,sha256=hTlqaBCiMe9YHFZs7gcyXMVRTsR0pY23LQN15D0ZOY0,10757
|
|
40
41
|
aye/model/snapshot/git_ref_backend.py,sha256=mY4yJiZ0ER4pgMzCgiTPQiKoVGCgWravNrPFue_zwLw,21356
|
|
@@ -42,7 +43,7 @@ aye/plugins/__init__.py,sha256=dSTxs461ICx0O1tbCBCca0W_7QIAa0Yt9PQhHiT5uZQ,173
|
|
|
42
43
|
aye/plugins/at_file_completer.py,sha256=uNS4gWpfKvn9_nGxZbhQVjVg_S82g977gfBR-pL3XrQ,19582
|
|
43
44
|
aye/plugins/auto_detect_mask.py,sha256=gZKH4qkR-A73uKpMkPXhlgI452Ae_2YG1nHtaIkOvwM,6864
|
|
44
45
|
aye/plugins/completer.py,sha256=qhxke5Q76P2u0LojSIL3V48RTNG5tWL-5-TK5tNutrE,13893
|
|
45
|
-
aye/plugins/local_model.py,sha256=
|
|
46
|
+
aye/plugins/local_model.py,sha256=q0RjSjLhEQcDMOCLAK6k1YCW5ECrvdT_g0lKRHMX-AE,14810
|
|
46
47
|
aye/plugins/offline_llm.py,sha256=qFmd1e8Lbl7yiMgXpXjOQkQTNxOk0_WXU7km2DTKXGY,13357
|
|
47
48
|
aye/plugins/plugin_base.py,sha256=t5hTOnA0dZC237BnseAgdXbOqErlSCNLUo_Uul09TSw,1673
|
|
48
49
|
aye/plugins/shell_executor.py,sha256=a0mlZnQeURONdtPM7iageTcQ8PiNLQbjxoY54EsS32o,7502
|
|
@@ -50,12 +51,12 @@ aye/plugins/slash_completer.py,sha256=MyrDTC_KwVWhtD_kpHbu0WjSjmSAWp36PwOBQczSuX
|
|
|
50
51
|
aye/presenter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
52
|
aye/presenter/cli_ui.py,sha256=8oHqQiMHO8hHXCTdqWoquMkJBshl2_3-YWN6SQnlbKg,8449
|
|
52
53
|
aye/presenter/diff_presenter.py,sha256=cbxfOEqGomPTDvQpKdybfYeNUD2DYVAl85j1uy5--Ww,12512
|
|
53
|
-
aye/presenter/repl_ui.py,sha256=
|
|
54
|
+
aye/presenter/repl_ui.py,sha256=PVENlAQM_tm_k2dANsmQH6I8ATMVXhrdj_hNzc38pSw,8156
|
|
54
55
|
aye/presenter/streaming_ui.py,sha256=_3tBEuNH9UQ9Gyq2yuvRfX4SWVkcGMYirEUGj-MXVJ0,12768
|
|
55
56
|
aye/presenter/ui_utils.py,sha256=6KXR4_ZZZUdF5pCHrPqO8yywlQk7AOzWe-2B4Wj_-ZQ,5441
|
|
56
|
-
ayechat_dev-0.36.9.
|
|
57
|
-
ayechat_dev-0.36.9.
|
|
58
|
-
ayechat_dev-0.36.9.
|
|
59
|
-
ayechat_dev-0.36.9.
|
|
60
|
-
ayechat_dev-0.36.9.
|
|
61
|
-
ayechat_dev-0.36.9.
|
|
57
|
+
ayechat_dev-0.36.9.20260204011001.dist-info/licenses/LICENSE,sha256=U1ou6lkMKmPo16-E9YowIu3goU7sOWKUprGo0AOA72s,1065
|
|
58
|
+
ayechat_dev-0.36.9.20260204011001.dist-info/METADATA,sha256=c6g026ZNTxo1IUHsgXnRCWdyJZnX_ddUTwwo4i4mvtc,7718
|
|
59
|
+
ayechat_dev-0.36.9.20260204011001.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
60
|
+
ayechat_dev-0.36.9.20260204011001.dist-info/entry_points.txt,sha256=KGsOma6szoefNN6vHozg3Pbf1fjZ7ZbmwrOiVwBd0Ik,41
|
|
61
|
+
ayechat_dev-0.36.9.20260204011001.dist-info/top_level.txt,sha256=7WZL0LOx4-GKKvgU1mtI5s4Dhk2OdieVZZvVnxFJHr8,4
|
|
62
|
+
ayechat_dev-0.36.9.20260204011001.dist-info/RECORD,,
|
{ayechat_dev-0.36.9.20260201141756.dist-info → ayechat_dev-0.36.9.20260204011001.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|