ayechat 0.36.8__py3-none-any.whl → 0.37.0__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.
@@ -15,6 +15,11 @@ from aye.model.snapshot import apply_updates
15
15
  from aye.model.file_processor import filter_unchanged_files, make_paths_relative
16
16
  from aye.model.models import LLMResponse
17
17
  from aye.model.auth import get_user_config
18
+ from aye.model.write_validator import (
19
+ check_files_against_ignore_patterns,
20
+ is_strict_mode_enabled,
21
+ format_ignored_files_warning,
22
+ )
18
23
 
19
24
 
20
25
  _HAS_USED_RESTORE_KEY = "has_used_restore"
@@ -91,16 +96,36 @@ def process_llm_response(
91
96
  if not updated_files:
92
97
  print_no_files_changed(console)
93
98
  else:
94
- # Apply updates to the model (Model update)
95
- try:
96
- apply_updates(updated_files, prompt)
97
- file_names = [item.get("file_name") for item in updated_files if "file_name" in item]
98
- if file_names:
99
- # Update the view
100
- print_files_updated(console, file_names)
101
- _maybe_print_restore_tip(conf, console)
102
- except Exception as e:
103
- rprint(f"[red]Error applying updates:[/] {e}")
99
+ # Check files against ignore patterns (issue #50)
100
+ root_path = Path(conf.root) if hasattr(conf, 'root') else Path.cwd()
101
+ allowed_files, ignored_files = check_files_against_ignore_patterns(
102
+ updated_files, root_path
103
+ )
104
+
105
+ # Handle ignored files
106
+ strict_mode = is_strict_mode_enabled()
107
+ if ignored_files:
108
+ warning_msg = format_ignored_files_warning(ignored_files, strict_mode)
109
+ console.print(Padding(warning_msg, (1, 4, 0, 4)))
110
+
111
+ if strict_mode:
112
+ # In strict mode, only write allowed files
113
+ updated_files = allowed_files
114
+ # In non-strict mode, continue with all files (just warned)
115
+
116
+ if not updated_files:
117
+ print_no_files_changed(console)
118
+ else:
119
+ # Apply updates to the model (Model update)
120
+ try:
121
+ apply_updates(updated_files, prompt)
122
+ file_names = [item.get("file_name") for item in updated_files if "file_name" in item]
123
+ if file_names:
124
+ # Update the view
125
+ print_files_updated(console, file_names)
126
+ _maybe_print_restore_tip(conf, console)
127
+ except Exception as e:
128
+ rprint(f"[red]Error applying updates:[/] {e}")
104
129
 
105
130
  return new_chat_id
106
131
 
aye/controller/repl.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import json
3
3
  from pathlib import Path
4
- from typing import Optional, Any
4
+ from typing import Optional, Any, List
5
5
  import shlex
6
6
  import threading
7
7
  import glob
@@ -252,6 +252,34 @@ def create_prompt_session(completer: Any, completion_style: str = "readline") ->
252
252
  )
253
253
 
254
254
 
255
+ def _execute_forced_shell_command(command: str, args: List[str], conf: Any) -> None:
256
+ """Execute a shell command with force flag (bypasses command validation).
257
+
258
+ Used when user prefixes input with '!' to force shell execution.
259
+
260
+ Args:
261
+ command: The command to execute
262
+ args: List of arguments to pass to the command
263
+ conf: Configuration object with plugin_manager
264
+ """
265
+ telemetry.record_command(command, has_args=len(args) > 0, prefix=_CMD_PREFIX)
266
+ shell_response = conf.plugin_manager.handle_command(
267
+ "execute_shell_command",
268
+ {"command": command, "args": args, "force": True}
269
+ )
270
+ if shell_response is not None:
271
+ if "stdout" in shell_response or "stderr" in shell_response:
272
+ if shell_response.get("stdout", "").strip():
273
+ rprint(shell_response["stdout"])
274
+ if shell_response.get("stderr", "").strip():
275
+ rprint(f"[yellow]{shell_response['stderr']}[/]")
276
+ if "error" in shell_response:
277
+ rprint(f"[red]Error:[/] {shell_response['error']}")
278
+ elif "message" in shell_response:
279
+ rprint(shell_response["message"])
280
+ else:
281
+ rprint(f"[red]Error:[/] Failed to execute shell command")
282
+
255
283
 
256
284
  def chat_repl(conf: Any) -> None:
257
285
  is_first_run = run_first_time_tutorial_if_needed()
@@ -327,6 +355,14 @@ def chat_repl(conf: Any) -> None:
327
355
  chat_id = new_chat_id
328
356
  continue
329
357
 
358
+ # Check for '!' prefix - force shell execution
359
+ force_shell = False
360
+ if prompt.strip().startswith('!'):
361
+ force_shell = True
362
+ prompt = prompt.strip()[1:] # Remove the '!'
363
+ if not prompt.strip():
364
+ continue # Nothing after the '!', skip
365
+
330
366
  if not prompt.strip():
331
367
  continue
332
368
  tokens = shlex.split(prompt.strip(), posix=False)
@@ -340,6 +376,11 @@ def chat_repl(conf: Any) -> None:
340
376
 
341
377
  original_first, lowered_first = tokens[0], tokens[0].lower()
342
378
 
379
+ # If force_shell is True, execute as shell command directly and skip all other checks
380
+ if force_shell:
381
+ _execute_forced_shell_command(original_first, tokens[1:], conf)
382
+ continue
383
+
343
384
  # Normalize slash-prefixed commands: /restore -> restore, /model -> model, etc.
344
385
  if lowered_first.startswith('/'):
345
386
  lowered_first = lowered_first[1:] # Remove leading slash
@@ -28,6 +28,7 @@ from .index_manager_state import (
28
28
  ProgressTracker,
29
29
  InitializationCoordinator,
30
30
  ErrorHandler,
31
+ _is_corruption_error,
31
32
  )
32
33
  from .index_manager_executor import PhaseExecutor
33
34
 
@@ -242,7 +243,15 @@ class IndexManager: # pylint: disable=too-many-instance-attributes
242
243
  deleted = get_deleted_files(current_paths, old_index)
243
244
  if deleted:
244
245
  self._error_handler.info(f"Deleted: {len(deleted)} file(s) from index.")
245
- vector_db.delete_from_index(self._init_coordinator.collection, deleted)
246
+ try:
247
+ vector_db.delete_from_index(self._init_coordinator.collection, deleted)
248
+ except Exception as e:
249
+ if _is_corruption_error(e):
250
+ rprint(f"[yellow]Detected index corruption during delete: {e}[/]")
251
+ self._init_coordinator.reset_and_recover()
252
+ # Don't re-raise, recovery will rebuild the index
253
+ else:
254
+ raise
246
255
 
247
256
  def _update_state_after_categorization(
248
257
  self,
@@ -549,9 +558,21 @@ class IndexManager: # pylint: disable=too-many-instance-attributes
549
558
  if not self._init_coordinator.collection:
550
559
  return []
551
560
 
552
- return vector_db.query_index(
553
- collection=self._init_coordinator.collection,
554
- query_text=query_text,
555
- n_results=n_results,
556
- min_relevance=min_relevance
557
- )
561
+ try:
562
+ return vector_db.query_index(
563
+ collection=self._init_coordinator.collection,
564
+ query_text=query_text,
565
+ n_results=n_results,
566
+ min_relevance=min_relevance
567
+ )
568
+ except Exception as e:
569
+ if _is_corruption_error(e):
570
+ rprint(f"[yellow]Detected index corruption during query: {e}[/]")
571
+ if self._init_coordinator.reset_and_recover():
572
+ # Recovery succeeded, index will rebuild in background
573
+ # Return empty results for this query
574
+ return []
575
+ # Recovery failed, code search disabled
576
+ return []
577
+ # Not a corruption error, re-raise
578
+ raise
@@ -267,6 +267,13 @@ _CORRUPTION_INDICATORS = (
267
267
  "OperationalError",
268
268
  "DatabaseError",
269
269
  "IntegrityError",
270
+ # HNSW-related errors (ChromaDB internal index corruption)
271
+ # See: https://github.com/acrotron/aye-chat/issues/203
272
+ "hnsw",
273
+ "segment reader",
274
+ "compactor",
275
+ "executing plan",
276
+ "backfill",
270
277
  )
271
278
 
272
279
 
@@ -399,22 +406,22 @@ class InitializationCoordinator:
399
406
  def _attempt_recovery(self) -> bool:
400
407
  """
401
408
  Attempt to recover from ChromaDB corruption.
402
-
409
+
403
410
  Strategy:
404
411
  1. Quarantine the corrupt chroma_db directory
405
412
  2. Invalidate the hash index (so files get re-indexed)
406
413
  3. Retry initialization with fresh DB
407
-
414
+
408
415
  Returns:
409
416
  True if recovery succeeded, False otherwise
410
417
  """
411
418
  from aye.model import vector_db
412
-
419
+
413
420
  self._recovery_attempted = True
414
421
  timestamp = int(time.time())
415
-
422
+
416
423
  rprint("[yellow]Attempting automatic recovery...[/]")
417
-
424
+
418
425
  # Step 1: Quarantine corrupt ChromaDB
419
426
  chroma_path = self.config.chroma_db_path
420
427
  if chroma_path.exists():
@@ -429,7 +436,7 @@ class InitializationCoordinator:
429
436
  shutil.rmtree(str(chroma_path), ignore_errors=True)
430
437
  except Exception:
431
438
  pass
432
-
439
+
433
440
  # Step 2: Invalidate hash index (so all files get re-indexed)
434
441
  hash_index_path = self.config.hash_index_path
435
442
  if hash_index_path.exists():
@@ -443,7 +450,7 @@ class InitializationCoordinator:
443
450
  hash_index_path.unlink(missing_ok=True)
444
451
  except Exception:
445
452
  pass
446
-
453
+
447
454
  # Step 3: Retry initialization
448
455
  try:
449
456
  rprint("[cyan]Reinitializing code search index...[/]")
@@ -459,6 +466,26 @@ class InitializationCoordinator:
459
466
  self.collection = None
460
467
  return False
461
468
 
469
+ def reset_and_recover(self) -> bool:
470
+ """
471
+ Reset state and attempt recovery from corruption detected during operations.
472
+
473
+ This is called when corruption is detected during query/update operations
474
+ (not during initialization). It resets the initialization state and
475
+ attempts recovery.
476
+
477
+ Returns:
478
+ True if recovery succeeded, False otherwise
479
+ """
480
+ with self._lock:
481
+ # Reset state to allow re-initialization
482
+ self._is_initialized = False
483
+ self._recovery_attempted = False
484
+ self.collection = None
485
+
486
+ # Now attempt recovery
487
+ return self._attempt_recovery()
488
+
462
489
 
463
490
  # =============================================================================
464
491
  # Error Handler
@@ -0,0 +1,105 @@
1
+ """Validates file writes against ignore patterns.
2
+
3
+ This module provides functionality to check if files being written
4
+ match .gitignore or .ayeignore patterns, and optionally block such writes.
5
+
6
+ See: https://github.com/acrotron/aye-chat/issues/50
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import List, Dict, Tuple
11
+
12
+ from aye.model.ignore_patterns import load_ignore_patterns
13
+ from aye.model.auth import get_user_config
14
+
15
+
16
+ # Config key for strict mode (block writes to ignored files)
17
+ BLOCK_IGNORED_WRITES_KEY = "block_ignored_file_writes"
18
+
19
+
20
+ def is_strict_mode_enabled() -> bool:
21
+ """Check if strict mode is enabled (block writes to ignored files).
22
+
23
+ Can be set via:
24
+ - Environment variable: AYE_BLOCK_IGNORED_FILE_WRITES=on
25
+ - Config file (~/.ayecfg): block_ignored_file_writes=on
26
+
27
+ Returns:
28
+ True if strict mode is enabled, False otherwise (default)
29
+ """
30
+ value = get_user_config(BLOCK_IGNORED_WRITES_KEY, "off")
31
+ return str(value).lower() in ("on", "true", "1", "yes")
32
+
33
+
34
+ def check_files_against_ignore_patterns(
35
+ files: List[Dict[str, str]],
36
+ root_path: Path
37
+ ) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]:
38
+ """Check which files match ignore patterns.
39
+
40
+ Args:
41
+ files: List of file dicts with 'file_name' and 'file_content' keys
42
+ root_path: Project root path for loading ignore patterns
43
+
44
+ Returns:
45
+ Tuple of (allowed_files, ignored_files)
46
+ - allowed_files: Files that don't match any ignore pattern
47
+ - ignored_files: Files that match ignore patterns
48
+ """
49
+ if not files:
50
+ return [], []
51
+
52
+ ignore_spec = load_ignore_patterns(root_path)
53
+
54
+ allowed_files = []
55
+ ignored_files = []
56
+
57
+ for file_dict in files:
58
+ file_name = file_dict.get("file_name", "")
59
+ if not file_name:
60
+ continue
61
+
62
+ # Normalize path for matching
63
+ # The pathspec library expects forward slashes
64
+ rel_path = file_name.replace("\\", "/")
65
+
66
+ if ignore_spec.match_file(rel_path):
67
+ ignored_files.append(file_dict)
68
+ else:
69
+ allowed_files.append(file_dict)
70
+
71
+ return allowed_files, ignored_files
72
+
73
+
74
+ def format_ignored_files_warning(
75
+ ignored_files: List[Dict[str, str]],
76
+ strict_mode: bool
77
+ ) -> str:
78
+ """Format a warning message about ignored files.
79
+
80
+ Args:
81
+ ignored_files: List of file dicts that match ignore patterns
82
+ strict_mode: Whether strict mode is enabled
83
+
84
+ Returns:
85
+ Formatted warning message
86
+ """
87
+ file_names = [f.get("file_name", "unknown") for f in ignored_files]
88
+ file_list = ", ".join(file_names)
89
+
90
+ if strict_mode:
91
+ msg = (
92
+ f"[yellow]Blocked write to ignored file(s): {file_list}[/]\n"
93
+ "[dim]These files match patterns in .gitignore or .ayeignore and were "
94
+ "not written.[/]"
95
+ )
96
+ else:
97
+ msg = (
98
+ f"[yellow]Warning: Writing to ignored file(s): {file_list}[/]\n"
99
+ "[dim]These files match patterns in .gitignore or .ayeignore. "
100
+ "Since they weren't read into context, their original content will be overwritten.[/]\n"
101
+ "[dim]To block writes to ignored files, set [bold]block_ignored_file_writes=on[/bold] "
102
+ "in ~/.ayecfg or use [bold]AYE_BLOCK_IGNORED_FILE_WRITES=on[/bold] environment variable.[/]"
103
+ )
104
+
105
+ return msg
@@ -10,7 +10,7 @@ from rich import print as rprint
10
10
 
11
11
  class ShellExecutorPlugin(Plugin):
12
12
  name = "shell_executor"
13
- version = "1.0.2" # Fixed Windows localization bug in command detection
13
+ version = "1.0.3" # Added force parameter for ! prefix execution
14
14
  premium = "free"
15
15
 
16
16
  # Known interactive commands that require a TTY (add more as needed)
@@ -143,15 +143,17 @@ class ShellExecutorPlugin(Plugin):
143
143
  "returncode": e.returncode
144
144
  }
145
145
  except FileNotFoundError:
146
- return None # Command not found
146
+ return {"error": f"Command not found: {command}", "stdout": "", "stderr": "", "returncode": 127}
147
147
 
148
148
  def on_command(self, command_name: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
149
149
  """Handle shell command execution through plugin system."""
150
150
  if command_name == "execute_shell_command":
151
151
  command = params.get("command", "")
152
152
  args = params.get("args", [])
153
+ force = params.get("force", False)
153
154
 
154
- if not self._is_valid_command(command):
155
+ # If force is False, validate the command exists first
156
+ if not force and not self._is_valid_command(command):
155
157
  return None # Command not found or not executable
156
158
 
157
159
  full_cmd_str = self._build_full_cmd(command, args)
aye/presenter/repl_ui.py CHANGED
@@ -87,7 +87,7 @@ def print_help_message():
87
87
  commands = [
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
- ("blog <intent>", "Generate a technical deep-dive blog post derived from the current chat session and write it to blog.md."),
90
+ ("!command", "Force shell execution (e.g., \"!echo hello\")."),
91
91
  ("model", "Select a different model. Selection will persist between sessions."),
92
92
  (r"verbose \[on|off]", "Toggle verbose mode to increase or decrease chattiness (on/off, persists between sessions)"),
93
93
  (r"completion \[readline|multi]", "Switch auto-completion style (readline or multi, persists between sessions)"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ayechat
3
- Version: 0.36.8
3
+ Version: 0.37.0
4
4
  Summary: Aye Chat: Terminal-first AI Code Generator
5
5
  Author-email: "Acrotron, Inc." <info@acrotron.com>
6
6
  License: MIT
@@ -5,10 +5,10 @@ aye/__main_chat__.py,sha256=R6RaidxG3Px5TaYxcoWAuIleE5KUZlceneUB6u_9UVU,1066
5
5
  aye/controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  aye/controller/command_handlers.py,sha256=C-6beHXVhW8vR_AbH4dCPl794ycFNVGuMBQiHnH4naI,13438
7
7
  aye/controller/commands.py,sha256=sXmK_sgNBrw9Fs7mKcr93-wsu740ZlvWSisQfS-1EUE,12278
8
- aye/controller/llm_handler.py,sha256=9Am1vK2tv5GLQcqIzdhopjxB8iGbc9p6_Qhet5FB31U,4087
8
+ aye/controller/llm_handler.py,sha256=BSBab6onF9BYiFndYW1647eEy377Vt52trzu0Qjm4bQ,5075
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=FOw8jH2gJp8DcNrSiUZkwrqB-qtDZ4b_9HwwpK4jiHY,24697
11
+ aye/controller/repl.py,sha256=UCgH4bdL3AUYjgD6KPtIOeysCNaYbs0TvEwS3Zql8kY,26552
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
@@ -27,11 +27,12 @@ aye/model/source_collector.py,sha256=KU84H-N7Ypj5UFKFJAqg_wa_gXyQRB5alEG_5wYeV1o
27
27
  aye/model/telemetry.py,sha256=qHnwxzTjDe7gbEah89qfKM4nKvBNa7nU_vfybFJTtRo,3255
28
28
  aye/model/vector_db.py,sha256=Bw5SKuclGR2wWKfyx7gIWS0UngjXkr5kUGnNIYtTjlw,6329
29
29
  aye/model/version_checker.py,sha256=cLAPF9tn-LyI4Q16UdDAj27BenhuzhL4K6-0nxw12IA,6169
30
+ aye/model/write_validator.py,sha256=_tNjR8iL56TxdgIrF2M0IaZz_tY8MiDrqd6F7lVVIlE,3345
30
31
  aye/model/index_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- aye/model/index_manager/index_manager.py,sha256=5cW--ivSY1fOg9gSpggTYzgLe8-MYHOu2zI9Wj6EdzA,19955
32
+ aye/model/index_manager/index_manager.py,sha256=TkPguukV3PqJSVNEVxsjrRdUuTdRPUjcCTYppaeuV1I,20912
32
33
  aye/model/index_manager/index_manager_executor.py,sha256=rSI8OX9bFld5ta-ajfT8eWU5taYJ7ovr0hA7hH0meqI,9464
33
34
  aye/model/index_manager/index_manager_file_ops.py,sha256=petgGU0ZtXOvLkHAQuzSbzqNIew5HygBOAy4yG6rTlk,7656
34
- aye/model/index_manager/index_manager_state.py,sha256=8WILwoQVs9TqLKrZdIZEw54sjQ3HH6CctUL_qWZnYXM,16742
35
+ aye/model/index_manager/index_manager_state.py,sha256=LflyC8UbE8fT7Tyag1ueh7TBisPa15cRhs0JI1-jVEw,17587
35
36
  aye/model/index_manager/index_manager_utils.py,sha256=B4NCYuScxQcG46WoUEx8mxfWKDjTlrSf3coRQCydiBM,4615
36
37
  aye/model/snapshot/__init__.py,sha256=Lqq-W5RBGxu3_7O5DzW_qY5iOF9MhogA46BAqEdu6Mw,6958
37
38
  aye/model/snapshot/base.py,sha256=5fzxM_85avxHwocv-w6PwlxDPzvMdTbuoUE_9P6D844,2009
@@ -44,17 +45,17 @@ aye/plugins/completer.py,sha256=qhxke5Q76P2u0LojSIL3V48RTNG5tWL-5-TK5tNutrE,1389
44
45
  aye/plugins/local_model.py,sha256=Jj4bHiPYaLMx6zTrKamBPrkiGQQ787jWz0F4ojRCjlQ,14394
45
46
  aye/plugins/offline_llm.py,sha256=qFmd1e8Lbl7yiMgXpXjOQkQTNxOk0_WXU7km2DTKXGY,13357
46
47
  aye/plugins/plugin_base.py,sha256=t5hTOnA0dZC237BnseAgdXbOqErlSCNLUo_Uul09TSw,1673
47
- aye/plugins/shell_executor.py,sha256=B4R04pJsjS6uy7z4MP_WcbxMPB6ct8V3BNFp9JMldrE,7317
48
+ aye/plugins/shell_executor.py,sha256=a0mlZnQeURONdtPM7iageTcQ8PiNLQbjxoY54EsS32o,7502
48
49
  aye/plugins/slash_completer.py,sha256=MyrDTC_KwVWhtD_kpHbu0WjSjmSAWp36PwOBQczSuXA,2252
49
50
  aye/presenter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
51
  aye/presenter/cli_ui.py,sha256=8oHqQiMHO8hHXCTdqWoquMkJBshl2_3-YWN6SQnlbKg,8449
51
52
  aye/presenter/diff_presenter.py,sha256=cbxfOEqGomPTDvQpKdybfYeNUD2DYVAl85j1uy5--Ww,12512
52
- aye/presenter/repl_ui.py,sha256=OTJlcqNdNoaAwfEUqallig45rIzMoKa0dUSxk6Io4jU,7979
53
+ aye/presenter/repl_ui.py,sha256=J6QlcFa_qvV00NP1OS5xyhDuJcfNcCrADZq9ZFY2GKQ,7917
53
54
  aye/presenter/streaming_ui.py,sha256=_3tBEuNH9UQ9Gyq2yuvRfX4SWVkcGMYirEUGj-MXVJ0,12768
54
55
  aye/presenter/ui_utils.py,sha256=6KXR4_ZZZUdF5pCHrPqO8yywlQk7AOzWe-2B4Wj_-ZQ,5441
55
- ayechat-0.36.8.dist-info/licenses/LICENSE,sha256=U1ou6lkMKmPo16-E9YowIu3goU7sOWKUprGo0AOA72s,1065
56
- ayechat-0.36.8.dist-info/METADATA,sha256=okYdZu_eftFmX9Vrq0v4sDR3X0nahKOxC3jCwyNhZhw,7699
57
- ayechat-0.36.8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
58
- ayechat-0.36.8.dist-info/entry_points.txt,sha256=KGsOma6szoefNN6vHozg3Pbf1fjZ7ZbmwrOiVwBd0Ik,41
59
- ayechat-0.36.8.dist-info/top_level.txt,sha256=7WZL0LOx4-GKKvgU1mtI5s4Dhk2OdieVZZvVnxFJHr8,4
60
- ayechat-0.36.8.dist-info/RECORD,,
56
+ ayechat-0.37.0.dist-info/licenses/LICENSE,sha256=U1ou6lkMKmPo16-E9YowIu3goU7sOWKUprGo0AOA72s,1065
57
+ ayechat-0.37.0.dist-info/METADATA,sha256=8BCRHQJzQlqt1wq05u1XNXJrrxc42Y0Hkw0we25mBUE,7699
58
+ ayechat-0.37.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
59
+ ayechat-0.37.0.dist-info/entry_points.txt,sha256=KGsOma6szoefNN6vHozg3Pbf1fjZ7ZbmwrOiVwBd0Ik,41
60
+ ayechat-0.37.0.dist-info/top_level.txt,sha256=7WZL0LOx4-GKKvgU1mtI5s4Dhk2OdieVZZvVnxFJHr8,4
61
+ ayechat-0.37.0.dist-info/RECORD,,