tass 0.1.4__py3-none-any.whl → 0.1.6__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.
src/app.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import os
3
3
  import subprocess
4
+ from pathlib import Path
4
5
 
5
6
  import requests
6
7
  from rich.console import Console
@@ -56,7 +57,7 @@ class TassApp:
56
57
  console.print(f"[red]Unable to verify new host {self.host}. Continuing with it anyway.[/red]")
57
58
 
58
59
  def summarize(self):
59
- max_messages = 10
60
+ max_messages = 20
60
61
  if len(self.messages) <= max_messages:
61
62
  return
62
63
 
@@ -124,16 +125,30 @@ class TassApp:
124
125
  return self.call_llm()
125
126
 
126
127
  def read_file(self, path: str, start: int = 1) -> str:
127
- console.print(f" Reading file [bold]{path}[/]...")
128
+ if start == 1:
129
+ console.print(f" └ Reading file [bold]{path}[/]...")
130
+ else:
131
+ console.print(f" └ Reading file [bold]{path}[/] (from line {start})...")
128
132
 
129
133
  try:
130
- with open(path) as f:
131
- out = f.read()
134
+ result = subprocess.run(
135
+ f"cat -n {path}",
136
+ shell=True,
137
+ capture_output=True,
138
+ text=True,
139
+ )
132
140
  except Exception as e:
133
141
  console.print(" [red]read_file failed[/red]")
134
142
  console.print(f" [red]{str(e)}[/red]")
135
143
  return f"read_file failed: {str(e)}"
136
144
 
145
+ out = result.stdout
146
+ err = result.stderr
147
+ if result.returncode != 0:
148
+ console.print(" [red]read_file failed[/red]")
149
+ console.print(f" [red]{err}[/red]")
150
+ return f"read_file failed: {err}"
151
+
137
152
  lines = []
138
153
  line_num = 1
139
154
  for line in out.split("\n"):
@@ -151,11 +166,52 @@ class TassApp:
151
166
  console.print(" [green]Command succeeded[/green]")
152
167
  return "".join(lines)
153
168
 
154
- def edit_file(self, path: str, find: str, replace: str) -> str:
155
- find_with_minuses = "\n".join([f"-{line}" for line in find.split("\n")])
156
- replace_with_pluses = "\n".join([f"+{line}" for line in replace.split("\n")])
169
+ def edit_file(self, path: str, edits: list[dict]) -> str:
170
+ for edit in edits:
171
+ edit["applied"] = False
172
+
173
+ def find_edit(n: int) -> dict | None:
174
+ for edit in edits:
175
+ if edit["line_start"] <= n <= edit["line_end"]:
176
+ return edit
177
+
178
+ return None
179
+
180
+ file_exists = Path(path).exists()
181
+ if file_exists:
182
+ with open(path, "r") as f:
183
+ original_content = f.read()
184
+ else:
185
+ original_content = ""
186
+
187
+ final_lines = []
188
+ original_lines = original_content.split("\n")
189
+ diff_text = f"{'Editing' if file_exists else 'Creating'} {path}"
190
+ for i, line in enumerate(original_lines):
191
+ line_num = i + 1
192
+ edit = find_edit(line_num)
193
+ if not edit:
194
+ final_lines.append(line)
195
+ continue
196
+
197
+ if edit["applied"]:
198
+ continue
199
+
200
+ replace_lines = edit["replace"].split("\n")
201
+ final_lines.extend(replace_lines)
202
+ original_lines = original_content.split("\n")
203
+ replaced_lines = original_lines[edit["line_start"] - 1:edit["line_end"]]
204
+
205
+ prev_line_num = line_num if line_num == 1 else line_num - 1
206
+ line_before = "" if i == 0 else f" {original_lines[i - 1]}\n"
207
+ line_after = "" if i == len(original_lines) - 1 else f"\n {original_lines[i + 1]}"
208
+ replaced_with_minuses = "\n".join([f"-{line}" for line in replaced_lines]) if file_exists else ""
209
+ replace_with_pluses = "\n".join([f"+{line}" for line in edit["replace"].split("\n")])
210
+ 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}\n{replace_with_pluses}{line_after}"
211
+ edit["applied"] = True
212
+
157
213
  console.print()
158
- console.print(Markdown(f"```diff\nEditing {path}\n{find_with_minuses}\n{replace_with_pluses}\n```"))
214
+ console.print(Markdown(f"```diff\n{diff_text}\n```"))
159
215
  answer = console.input("\n[bold]Run?[/] ([bold]Y[/]/n): ").strip().lower()
160
216
  if answer not in ("yes", "y", ""):
161
217
  reason = console.input("Why not? (optional, press Enter to skip): ").strip()
@@ -163,18 +219,8 @@ class TassApp:
163
219
 
164
220
  console.print(" └ Running...")
165
221
  try:
166
- with open(path, "r") as f:
167
- original_content = f.read()
168
-
169
- if find not in original_content:
170
- console.print(" [red]edit_file failed[/red]")
171
- console.print(f" [red]edit_file failed:\n'{find}'\nnot found in file[/red]")
172
- return f"edit_file failed: '{find}' not found in file"
173
-
174
- new_content = original_content.replace(find, replace)
175
-
176
222
  with open(path, "w") as f:
177
- f.write(new_content)
223
+ f.write("\n".join(final_lines))
178
224
  except Exception as e:
179
225
  console.print(" [red]edit_file failed[/red]")
180
226
  console.print(f" [red]{str(e)}[/red]")
src/constants.py CHANGED
@@ -37,7 +37,7 @@ TOOLS = [
37
37
  "type": "function",
38
38
  "function": {
39
39
  "name": "edit_file",
40
- "description": "Edits a file. Replaces the instance of 'find' with 'replace'. 'find' and 'replace' are exact strings.The file must be read at least once before calling this tool.",
40
+ "description": "Edits (or creates) a file. Makes multiple replacements in one call. Each edit removes the contents between 'line_start' and 'line_end' inclusive and replaces it with 'replace'. If creating a file, only return a single edit where the line_start and line_end are both 1 and replace is the entire contents of the file.",
41
41
  "parameters": {
42
42
  "type": "object",
43
43
  "properties": {
@@ -45,16 +45,30 @@ TOOLS = [
45
45
  "type": "string",
46
46
  "description": "Relative path of the file",
47
47
  },
48
- "find": {
49
- "type": "string",
50
- "description": "The string to find",
51
- },
52
- "replace": {
53
- "type": "string",
54
- "description": "The string to replace with",
48
+ "edits": {
49
+ "type": "array",
50
+ "description": "List of edits to apply. Each edit must contain 'line_start', 'line_end', and 'replace'.",
51
+ "items": {
52
+ "type": "object",
53
+ "properties": {
54
+ "line_start": {
55
+ "type": "integer",
56
+ "description": "The first line to remove (inclusive)",
57
+ },
58
+ "line_end": {
59
+ "type": "integer",
60
+ "description": "The last line to remove (inclusive)",
61
+ },
62
+ "replace": {
63
+ "type": "string",
64
+ "description": "The string to replace with. Must have the correct spacing and indentation for all lines.",
65
+ },
66
+ },
67
+ "required": ["line_start", "line_end", "replace"],
68
+ },
55
69
  },
56
70
  },
57
- "required": ["path", "find", "replace"],
71
+ "required": ["path", "edits"],
58
72
  "$schema": "http://json-schema.org/draft-07/schema#",
59
73
  },
60
74
  },
@@ -63,7 +77,7 @@ TOOLS = [
63
77
  "type": "function",
64
78
  "function": {
65
79
  "name": "read_file",
66
- "description": "Read a file's contents (up to 1000 lines)",
80
+ "description": "Read a file's contents (up to 1000 lines). The output will be identical to calling `cat -n <path>` with preceding spaces, line number and a tab.",
67
81
  "parameters": {
68
82
  "type": "object",
69
83
  "properties": {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tass
3
- Version: 0.1.4
3
+ Version: 0.1.6
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
@@ -33,4 +33,4 @@ tass
33
33
 
34
34
  tass has only been tested with gpt-oss-120b using llama.cpp so far, but in theory any LLM with tool calling capabilities should work. By default, it will try connecting to http://localhost:8080. If you want to use another host, set the `TASS_HOST` environment variable.
35
35
 
36
- Once it's running, you can ask questions like "Can you create an empty file called test.txt?" and it will propose a command to run after user confirmation.
36
+ Once it's running, you can ask questions or give commands like "Create an empty file called test.txt" and it will propose a command to run after user confirmation.
@@ -0,0 +1,10 @@
1
+ src/__init__.py,sha256=tu2q9W5_pkq30l3tRMTGahColBAAubbLP6LaB3l3IFg,89
2
+ src/app.py,sha256=SZGIStkRskTraOARKR-sh8hjfQT7EXwJBG-oymIABhU,11466
3
+ src/cli.py,sha256=op3fYcyfek_KqCCiA-Zdlc9jVZSCi036whMmR2ZjjAs,76
4
+ src/constants.py,sha256=2MWn3-tvZjJ2xW68BE7S1V8CgqDuBt3cBG5Bx8ILrKY,4620
5
+ src/utils.py,sha256=rKq34DVmFbsWPy7R6Bfdvv1ztzFLPT4hUd8BFpPHjqs,681
6
+ tass-0.1.6.dist-info/METADATA,sha256=xu-OHc1sIrlDrxXVCUSdPmzYnjVam5tYB96EYJiTWCc,1079
7
+ tass-0.1.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
+ tass-0.1.6.dist-info/entry_points.txt,sha256=pviKuIOuHvaQ7_YiFxatJEY8XYfh3EzVWy4LJh0v-A0,38
9
+ tass-0.1.6.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
10
+ tass-0.1.6.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- src/__init__.py,sha256=tu2q9W5_pkq30l3tRMTGahColBAAubbLP6LaB3l3IFg,89
2
- src/app.py,sha256=sTPTwDc8KjF9RpL7W5Ix0D0n8ERcWsP1WEVzLWGOKco,9779
3
- src/cli.py,sha256=op3fYcyfek_KqCCiA-Zdlc9jVZSCi036whMmR2ZjjAs,76
4
- src/constants.py,sha256=A0_PwEYo1Dpf8UC6HV7JgQwU7GlZQxKhLgwgmyoc3WY,3478
5
- src/utils.py,sha256=rKq34DVmFbsWPy7R6Bfdvv1ztzFLPT4hUd8BFpPHjqs,681
6
- tass-0.1.4.dist-info/METADATA,sha256=K1hq1AlWam-s671Nhmb0-X4jj6qAOi8Lfw27wuDxAXA,1071
7
- tass-0.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
- tass-0.1.4.dist-info/entry_points.txt,sha256=pviKuIOuHvaQ7_YiFxatJEY8XYfh3EzVWy4LJh0v-A0,38
9
- tass-0.1.4.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
10
- tass-0.1.4.dist-info/RECORD,,
File without changes