tass 0.1.9__tar.gz → 0.1.11__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.
- {tass-0.1.9 → tass-0.1.11}/PKG-INFO +2 -1
- {tass-0.1.9 → tass-0.1.11}/pyproject.toml +2 -1
- {tass-0.1.9 → tass-0.1.11}/src/app.py +30 -16
- {tass-0.1.9 → tass-0.1.11}/src/constants.py +13 -8
- tass-0.1.11/src/utils.py +98 -0
- {tass-0.1.9 → tass-0.1.11}/uv.lock +24 -1
- tass-0.1.9/src/utils.py +0 -23
- {tass-0.1.9 → tass-0.1.11}/.gitignore +0 -0
- {tass-0.1.9 → tass-0.1.11}/.python-version +0 -0
- {tass-0.1.9 → tass-0.1.11}/LICENSE +0 -0
- {tass-0.1.9 → tass-0.1.11}/README.md +0 -0
- {tass-0.1.9 → tass-0.1.11}/assets/tass.gif +0 -0
- {tass-0.1.9 → tass-0.1.11}/src/__init__.py +0 -0
- {tass-0.1.9 → tass-0.1.11}/src/cli.py +0 -0
- {tass-0.1.9 → tass-0.1.11}/tests/test_utils.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tass
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: A terminal assistant that allows you to ask an LLM to run commands.
|
|
5
5
|
Project-URL: Homepage, https://github.com/cetincan0/tass
|
|
6
6
|
Author: Can Cetin
|
|
7
7
|
License: MIT
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: prompt-toolkit>=3.0.52
|
|
10
11
|
Requires-Dist: requests>=2.32.5
|
|
11
12
|
Requires-Dist: rich>=14.2.0
|
|
12
13
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tass"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.11"
|
|
4
4
|
description = "A terminal assistant that allows you to ask an LLM to run commands."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -8,6 +8,7 @@ license = {text = "MIT"}
|
|
|
8
8
|
authors = [{name = "Can Cetin"}]
|
|
9
9
|
|
|
10
10
|
dependencies = [
|
|
11
|
+
"prompt-toolkit>=3.0.52",
|
|
11
12
|
"requests>=2.32.5",
|
|
12
13
|
"rich>=14.2.0",
|
|
13
14
|
]
|
|
@@ -4,7 +4,7 @@ import subprocess
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
import requests
|
|
7
|
-
|
|
7
|
+
from prompt_toolkit import prompt
|
|
8
8
|
from rich.console import Console, Group
|
|
9
9
|
from rich.live import Live
|
|
10
10
|
from rich.markdown import Markdown
|
|
@@ -15,7 +15,11 @@ from src.constants import (
|
|
|
15
15
|
SYSTEM_PROMPT,
|
|
16
16
|
TOOLS,
|
|
17
17
|
)
|
|
18
|
-
from src.utils import
|
|
18
|
+
from src.utils import (
|
|
19
|
+
FileCompleter,
|
|
20
|
+
create_key_bindings,
|
|
21
|
+
is_read_only_command,
|
|
22
|
+
)
|
|
19
23
|
|
|
20
24
|
console = Console()
|
|
21
25
|
|
|
@@ -25,6 +29,8 @@ class TassApp:
|
|
|
25
29
|
def __init__(self):
|
|
26
30
|
self.messages: list[dict] = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
27
31
|
self.host = os.environ.get("TASS_HOST", "http://localhost:8080")
|
|
32
|
+
self.key_bindings = create_key_bindings()
|
|
33
|
+
self.file_completer = FileCompleter()
|
|
28
34
|
self.TOOLS_MAP = {
|
|
29
35
|
"execute": self.execute,
|
|
30
36
|
"read_file": self.read_file,
|
|
@@ -193,9 +199,9 @@ class TassApp:
|
|
|
193
199
|
self.messages.append(
|
|
194
200
|
{
|
|
195
201
|
"role": "assistant",
|
|
196
|
-
"content": content.strip()
|
|
197
|
-
"reasoning_content": reasoning_content.strip()
|
|
198
|
-
"tool_calls": list(tool_calls_map.values()) or
|
|
202
|
+
"content": content.strip(),
|
|
203
|
+
"reasoning_content": reasoning_content.strip(),
|
|
204
|
+
"tool_calls": list(tool_calls_map.values()) or [],
|
|
199
205
|
}
|
|
200
206
|
)
|
|
201
207
|
|
|
@@ -220,11 +226,12 @@ class TassApp:
|
|
|
220
226
|
self.messages.append({"role": "user", "content": str(e)})
|
|
221
227
|
return self.call_llm()
|
|
222
228
|
|
|
223
|
-
def read_file(self, path: str, start: int = 1) -> str:
|
|
224
|
-
if start == 1:
|
|
229
|
+
def read_file(self, path: str, start: int = 1, num_lines: int = 1000) -> str:
|
|
230
|
+
if start == 1 and num_lines == 1000:
|
|
225
231
|
console.print(f" └ Reading file [bold]{path}[/]...")
|
|
226
232
|
else:
|
|
227
|
-
|
|
233
|
+
last_line = start + num_lines - 1
|
|
234
|
+
console.print(f" └ Reading file [bold]{path}[/] (lines {start}-{last_line})...")
|
|
228
235
|
|
|
229
236
|
try:
|
|
230
237
|
result = subprocess.run(
|
|
@@ -256,12 +263,12 @@ class TassApp:
|
|
|
256
263
|
lines.append(line)
|
|
257
264
|
line_num += 1
|
|
258
265
|
|
|
259
|
-
if len(lines) >=
|
|
266
|
+
if len(lines) >= num_lines:
|
|
260
267
|
lines.append("... (truncated)")
|
|
261
268
|
break
|
|
262
269
|
|
|
263
270
|
console.print(" [green]Command succeeded[/green]")
|
|
264
|
-
return "".join(lines)
|
|
271
|
+
return "\n".join(lines)
|
|
265
272
|
|
|
266
273
|
def edit_file(self, path: str, edits: list[dict]) -> str:
|
|
267
274
|
for edit in edits:
|
|
@@ -294,8 +301,9 @@ class TassApp:
|
|
|
294
301
|
if edit["applied"]:
|
|
295
302
|
continue
|
|
296
303
|
|
|
297
|
-
replace_lines = edit["
|
|
298
|
-
|
|
304
|
+
replace_lines = edit["content"].split("\n")
|
|
305
|
+
if edit["content"]:
|
|
306
|
+
final_lines.extend(replace_lines)
|
|
299
307
|
original_lines = original_content.split("\n")
|
|
300
308
|
replaced_lines = original_lines[edit["line_start"] - 1:edit["line_end"]]
|
|
301
309
|
|
|
@@ -303,8 +311,10 @@ class TassApp:
|
|
|
303
311
|
line_before = "" if i == 0 else f" {original_lines[i - 1]}\n"
|
|
304
312
|
line_after = "" if edit["line_end"] == len(original_lines) else f"\n {original_lines[edit['line_end']]}"
|
|
305
313
|
replaced_with_minuses = "\n".join([f"-{line}" for line in replaced_lines]) if file_exists else ""
|
|
306
|
-
|
|
307
|
-
|
|
314
|
+
replaced_with_pluses = ""
|
|
315
|
+
if edit["content"]:
|
|
316
|
+
replaced_with_pluses = "\n" + "\n".join([f"+{line}" for line in edit["content"].split("\n")])
|
|
317
|
+
diff_text = f"{diff_text}\n\n@@ -{prev_line_num},{len(replaced_lines)} +{prev_line_num},{len(replace_lines)} @@\n{line_before}{replaced_with_minuses}{replaced_with_pluses}{line_after}"
|
|
308
318
|
edit["applied"] = True
|
|
309
319
|
|
|
310
320
|
console.print()
|
|
@@ -393,12 +403,16 @@ class TassApp:
|
|
|
393
403
|
try:
|
|
394
404
|
input_lines = []
|
|
395
405
|
while True:
|
|
396
|
-
input_line =
|
|
406
|
+
input_line = prompt(
|
|
407
|
+
"> ",
|
|
408
|
+
completer=self.file_completer,
|
|
409
|
+
complete_while_typing=True,
|
|
410
|
+
key_bindings=self.key_bindings,
|
|
411
|
+
)
|
|
397
412
|
if not input_line or input_line[-1] != "\\":
|
|
398
413
|
input_lines.append(input_line)
|
|
399
414
|
break
|
|
400
415
|
input_lines.append(input_line[:-1])
|
|
401
|
-
|
|
402
416
|
user_input = "\n".join(input_lines)
|
|
403
417
|
except KeyboardInterrupt:
|
|
404
418
|
console.print("\nBye!")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
cwd_path = Path.cwd().resolve()
|
|
4
4
|
|
|
5
5
|
SYSTEM_PROMPT = f"""You are tass, or Terminal Assistant, a helpful AI that executes shell commands based on natural-language requests.
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ If the user's request involves making changes to the filesystem such as creating
|
|
|
8
8
|
|
|
9
9
|
If a user asks for an answer or explanation to something instead of requesting to run a command, answer briefly and concisely. Do not supply extra information, suggestions, tips, or anything of the sort.
|
|
10
10
|
|
|
11
|
-
Current working directory: {
|
|
11
|
+
Current working directory: {cwd_path}"""
|
|
12
12
|
|
|
13
13
|
TOOLS = [
|
|
14
14
|
{
|
|
@@ -37,7 +37,7 @@ TOOLS = [
|
|
|
37
37
|
"type": "function",
|
|
38
38
|
"function": {
|
|
39
39
|
"name": "edit_file",
|
|
40
|
-
"description": "Edits (or creates) a file.
|
|
40
|
+
"description": "Edits (or creates) a file. Can make multiple edits in one call. Each edit replaces the contents between 'line_start' and 'line_end' inclusive with 'content'. If creating a file, only return a single edit where 'line_start' and 'line_end' are both 1 and 'content' is the entire contents of the file. You must use the read_file tool before editing a file.",
|
|
41
41
|
"parameters": {
|
|
42
42
|
"type": "object",
|
|
43
43
|
"properties": {
|
|
@@ -47,7 +47,7 @@ TOOLS = [
|
|
|
47
47
|
},
|
|
48
48
|
"edits": {
|
|
49
49
|
"type": "array",
|
|
50
|
-
"description": "List of edits to apply. Each edit must contain 'line_start', 'line_end', and '
|
|
50
|
+
"description": "List of edits to apply. Each edit must contain 'line_start', 'line_end', and 'content'.",
|
|
51
51
|
"items": {
|
|
52
52
|
"type": "object",
|
|
53
53
|
"properties": {
|
|
@@ -59,12 +59,12 @@ TOOLS = [
|
|
|
59
59
|
"type": "integer",
|
|
60
60
|
"description": "The last line to remove (inclusive)",
|
|
61
61
|
},
|
|
62
|
-
"
|
|
62
|
+
"content": {
|
|
63
63
|
"type": "string",
|
|
64
|
-
"description": "The
|
|
64
|
+
"description": "The content to replace with. Must have the correct spacing and indentation for all lines.",
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
|
-
"required": ["line_start", "line_end", "
|
|
67
|
+
"required": ["line_start", "line_end", "content"],
|
|
68
68
|
},
|
|
69
69
|
},
|
|
70
70
|
},
|
|
@@ -77,7 +77,7 @@ TOOLS = [
|
|
|
77
77
|
"type": "function",
|
|
78
78
|
"function": {
|
|
79
79
|
"name": "read_file",
|
|
80
|
-
"description": "Read a file's contents (
|
|
80
|
+
"description": "Read a file's contents (the first 1000 lines by default). When reading a file for the first time, do not change the defaults and always read the first 1000 lines unless you are absolutely certain of which lines need to be read. The output will be identical to calling `cat -n <path>` with preceding spaces, line number and a tab.",
|
|
81
81
|
"parameters": {
|
|
82
82
|
"type": "object",
|
|
83
83
|
"properties": {
|
|
@@ -90,6 +90,11 @@ TOOLS = [
|
|
|
90
90
|
"description": "Which line to start reading from",
|
|
91
91
|
"default": 1,
|
|
92
92
|
},
|
|
93
|
+
"num_lines": {
|
|
94
|
+
"type": "integer",
|
|
95
|
+
"description": "Number of lines to read, defaults to 1000",
|
|
96
|
+
"default": 1000,
|
|
97
|
+
},
|
|
93
98
|
},
|
|
94
99
|
"required": ["path"],
|
|
95
100
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
tass-0.1.11/src/utils.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from prompt_toolkit.completion import (
|
|
2
|
+
Completer,
|
|
3
|
+
Completion,
|
|
4
|
+
CompleteEvent,
|
|
5
|
+
)
|
|
6
|
+
from prompt_toolkit.document import Document
|
|
7
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
8
|
+
from prompt_toolkit.keys import Keys
|
|
9
|
+
|
|
10
|
+
from src.constants import (
|
|
11
|
+
READ_ONLY_COMMANDS,
|
|
12
|
+
cwd_path,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def is_read_only_command(command: str) -> bool:
|
|
16
|
+
"""A simple check to see if the command is only for reading files.
|
|
17
|
+
|
|
18
|
+
Not a comprehensive or foolproof check by any means, and will
|
|
19
|
+
return false negatives to be safe.
|
|
20
|
+
"""
|
|
21
|
+
if ">" in command:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
# Replace everything that potentially runs another command with a pipe
|
|
25
|
+
command = command.replace("&&", "|")
|
|
26
|
+
command = command.replace("||", "|")
|
|
27
|
+
command = command.replace(";", "|")
|
|
28
|
+
|
|
29
|
+
pipes = command.split("|")
|
|
30
|
+
for pipe in pipes:
|
|
31
|
+
if pipe.strip().split()[0] not in READ_ONLY_COMMANDS:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FileCompleter(Completer):
|
|
38
|
+
|
|
39
|
+
def get_completions(self, document: Document, complete_event: CompleteEvent) -> list[Completion]:
|
|
40
|
+
"""Return file completions for text after @ anywhere in input."""
|
|
41
|
+
text = document.text
|
|
42
|
+
cursor_pos = document.cursor_position
|
|
43
|
+
text_before_cursor = document.text_before_cursor
|
|
44
|
+
last_at_pos = text_before_cursor.rfind("@")
|
|
45
|
+
if last_at_pos == -1:
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
text_after_at = text[last_at_pos + 1 : cursor_pos]
|
|
49
|
+
|
|
50
|
+
base_dir = cwd_path
|
|
51
|
+
search_pattern = text_after_at
|
|
52
|
+
prefix = ""
|
|
53
|
+
|
|
54
|
+
if "/" in text_after_at:
|
|
55
|
+
dir_part, file_part = text_after_at.rsplit("/", 1)
|
|
56
|
+
base_dir = cwd_path / dir_part
|
|
57
|
+
search_pattern = file_part
|
|
58
|
+
prefix = dir_part + "/"
|
|
59
|
+
|
|
60
|
+
if not base_dir.is_dir():
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
dirs = []
|
|
65
|
+
files = []
|
|
66
|
+
for entry in sorted(base_dir.iterdir()):
|
|
67
|
+
name = entry.name
|
|
68
|
+
if search_pattern in name:
|
|
69
|
+
full_path = prefix + name
|
|
70
|
+
if entry.is_dir():
|
|
71
|
+
dirs.append(Completion(full_path, display=name, start_position=-len(text_after_at), style="Blue"))
|
|
72
|
+
else:
|
|
73
|
+
files.append(Completion(full_path, display=name, start_position=-len(text_after_at)))
|
|
74
|
+
|
|
75
|
+
return dirs + files
|
|
76
|
+
except (PermissionError, OSError):
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_key_bindings() -> KeyBindings:
|
|
81
|
+
bindings = KeyBindings()
|
|
82
|
+
|
|
83
|
+
@bindings.add(Keys.Backspace)
|
|
84
|
+
def backspace_with_completion(event):
|
|
85
|
+
"""Also trigger completion on backspace."""
|
|
86
|
+
buffer = event.current_buffer
|
|
87
|
+
buffer.delete_before_cursor(count=1)
|
|
88
|
+
text_before_cursor = buffer.document.text_before_cursor
|
|
89
|
+
last_at_pos = text_before_cursor.rfind("@")
|
|
90
|
+
if last_at_pos != -1:
|
|
91
|
+
buffer.start_completion()
|
|
92
|
+
|
|
93
|
+
@bindings.add(Keys.ControlC)
|
|
94
|
+
def ctrl_c_handler(event):
|
|
95
|
+
"""Ctrl+C should raise KeyboardInterrupt."""
|
|
96
|
+
raise KeyboardInterrupt()
|
|
97
|
+
|
|
98
|
+
return bindings
|
|
@@ -178,6 +178,18 @@ wheels = [
|
|
|
178
178
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
179
179
|
]
|
|
180
180
|
|
|
181
|
+
[[package]]
|
|
182
|
+
name = "prompt-toolkit"
|
|
183
|
+
version = "3.0.52"
|
|
184
|
+
source = { registry = "https://pypi.org/simple" }
|
|
185
|
+
dependencies = [
|
|
186
|
+
{ name = "wcwidth" },
|
|
187
|
+
]
|
|
188
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
|
|
189
|
+
wheels = [
|
|
190
|
+
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
|
191
|
+
]
|
|
192
|
+
|
|
181
193
|
[[package]]
|
|
182
194
|
name = "pygments"
|
|
183
195
|
version = "2.19.2"
|
|
@@ -261,9 +273,10 @@ wheels = [
|
|
|
261
273
|
|
|
262
274
|
[[package]]
|
|
263
275
|
name = "tass"
|
|
264
|
-
version = "0.1.
|
|
276
|
+
version = "0.1.11"
|
|
265
277
|
source = { editable = "." }
|
|
266
278
|
dependencies = [
|
|
279
|
+
{ name = "prompt-toolkit" },
|
|
267
280
|
{ name = "requests" },
|
|
268
281
|
{ name = "rich" },
|
|
269
282
|
]
|
|
@@ -276,6 +289,7 @@ dev = [
|
|
|
276
289
|
|
|
277
290
|
[package.metadata]
|
|
278
291
|
requires-dist = [
|
|
292
|
+
{ name = "prompt-toolkit", specifier = ">=3.0.52" },
|
|
279
293
|
{ name = "requests", specifier = ">=2.32.5" },
|
|
280
294
|
{ name = "rich", specifier = ">=14.2.0" },
|
|
281
295
|
]
|
|
@@ -352,3 +366,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787
|
|
|
352
366
|
wheels = [
|
|
353
367
|
{ url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" },
|
|
354
368
|
]
|
|
369
|
+
|
|
370
|
+
[[package]]
|
|
371
|
+
name = "wcwidth"
|
|
372
|
+
version = "0.2.14"
|
|
373
|
+
source = { registry = "https://pypi.org/simple" }
|
|
374
|
+
sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" }
|
|
375
|
+
wheels = [
|
|
376
|
+
{ url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
|
|
377
|
+
]
|
tass-0.1.9/src/utils.py
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from src.constants import READ_ONLY_COMMANDS
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def is_read_only_command(command: str) -> bool:
|
|
5
|
-
"""A simple check to see if the command is only for reading files.
|
|
6
|
-
|
|
7
|
-
Not a comprehensive or foolproof check by any means, and will
|
|
8
|
-
return false negatives to be safe.
|
|
9
|
-
"""
|
|
10
|
-
if ">" in command:
|
|
11
|
-
return False
|
|
12
|
-
|
|
13
|
-
# Replace everything that potentially runs another command with a pipe
|
|
14
|
-
command = command.replace("&&", "|")
|
|
15
|
-
command = command.replace("||", "|")
|
|
16
|
-
command = command.replace(";", "|")
|
|
17
|
-
|
|
18
|
-
pipes = command.split("|")
|
|
19
|
-
for pipe in pipes:
|
|
20
|
-
if pipe.strip().split()[0] not in READ_ONLY_COMMANDS:
|
|
21
|
-
return False
|
|
22
|
-
|
|
23
|
-
return True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|