weio-cli 0.5.0__tar.gz → 0.6.0__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.
- {weio_cli-0.5.0/src/weio_cli.egg-info → weio_cli-0.6.0}/PKG-INFO +1 -1
- {weio_cli-0.5.0 → weio_cli-0.6.0}/pyproject.toml +1 -1
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/__init__.py +1 -1
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/agent.py +30 -1
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/tools.py +13 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0/src/weio_cli.egg-info}/PKG-INFO +1 -1
- {weio_cli-0.5.0 → weio_cli-0.6.0}/tests/test_agent.py +15 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/LICENSE +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/README.md +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/setup.cfg +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/__main__.py +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/browser_login.py +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/cli.py +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/client.py +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/config.py +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/settings.py +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/tui.py +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli/updater.py +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/SOURCES.txt +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/dependency_links.txt +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/entry_points.txt +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/requires.txt +0 -0
- {weio_cli-0.5.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weio-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Weio — an agentic coding assistant that routes inference through your Weio account."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -9,6 +9,7 @@ UI is injected via callbacks so the TUI and one-shot runner share this engine.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
|
+
import platform
|
|
12
13
|
import re
|
|
13
14
|
from dataclasses import dataclass
|
|
14
15
|
from pathlib import Path
|
|
@@ -23,13 +24,26 @@ _BARE_RE = re.compile(r'\{[^{}]*"tool"\s*:\s*"[a-z_]+".*?\}', re.DOTALL)
|
|
|
23
24
|
|
|
24
25
|
READONLY_TOOLS = {"read_file", "list_dir", "search"}
|
|
25
26
|
WRITE_TOOLS = {"write_file", "edit_file"}
|
|
26
|
-
ALL_TOOLS = READONLY_TOOLS | WRITE_TOOLS | {"run_command", "finish"}
|
|
27
|
+
ALL_TOOLS = READONLY_TOOLS | WRITE_TOOLS | {"make_dir", "run_command", "finish"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _os_line() -> str:
|
|
31
|
+
sysname = platform.system() # 'Windows' | 'Darwin' | 'Linux'
|
|
32
|
+
if sysname == "Windows":
|
|
33
|
+
return ("Windows (cmd.exe). Use Windows path/command syntax. ALWAYS wrap any "
|
|
34
|
+
"path containing spaces in double quotes. Prefer the make_dir and "
|
|
35
|
+
"write_file tools over shell `mkdir`/redirection — they handle spaces safely.")
|
|
36
|
+
nice = "macOS" if sysname == "Darwin" else sysname
|
|
37
|
+
return (f"{nice} (POSIX shell). Quote any path containing spaces. Prefer make_dir "
|
|
38
|
+
"and write_file over shell mkdir/redirection.")
|
|
27
39
|
|
|
28
40
|
|
|
29
41
|
def system_prompt(root: Path) -> str:
|
|
30
42
|
return f"""You are Weio, an autonomous coding agent working in the user's project directory:
|
|
31
43
|
{root}
|
|
32
44
|
|
|
45
|
+
Environment: {_os_line()}
|
|
46
|
+
|
|
33
47
|
You complete the user's task by using TOOLS, one step at a time. On each turn, briefly
|
|
34
48
|
state what you are about to do (one or two sentences), then emit one or more tool calls
|
|
35
49
|
as fenced JSON blocks exactly like this:
|
|
@@ -42,6 +56,7 @@ TOOLS:
|
|
|
42
56
|
- read_file {{"path": "..."}} read a file
|
|
43
57
|
- list_dir {{"path": "."}} list a directory
|
|
44
58
|
- search {{"query": "...", "path": "."}} search file contents
|
|
59
|
+
- make_dir {{"path": "..."}} create a directory (handles spaces safely)
|
|
45
60
|
- write_file {{"path": "...", "content": "..."}} create/overwrite a file
|
|
46
61
|
- edit_file {{"path": "...", "search": "...", "replace": "..."}} replace an exact snippet
|
|
47
62
|
- run_command {{"command": "..."}} run a shell command in the project
|
|
@@ -118,6 +133,7 @@ class Agent:
|
|
|
118
133
|
self.approval = settings.get("approval", "suggest")
|
|
119
134
|
self.max_steps = int(settings.get("max_steps", 25))
|
|
120
135
|
self.max_tokens = int(settings.get("max_tokens", 4096))
|
|
136
|
+
self._failed_cmds: dict[str, int] = {}
|
|
121
137
|
|
|
122
138
|
# -- model call --
|
|
123
139
|
def _complete(self) -> str:
|
|
@@ -143,6 +159,8 @@ class Agent:
|
|
|
143
159
|
r = tools.list_dir(self.root, args.get("path", "."))
|
|
144
160
|
elif tool == "search":
|
|
145
161
|
r = tools.search_files(self.root, args.get("query", ""), args.get("path", "."))
|
|
162
|
+
elif tool == "make_dir":
|
|
163
|
+
r = tools.make_dir(self.root, args.get("path", ""))
|
|
146
164
|
elif tool == "write_file":
|
|
147
165
|
r = tools.write_file(self.root, args.get("path", ""), args.get("content", ""))
|
|
148
166
|
r = self._maybe_commit(tool, args, r)
|
|
@@ -156,6 +174,17 @@ class Agent:
|
|
|
156
174
|
r = ToolResult(False, "Command declined by user.", display=f"$ {cmd} (declined)")
|
|
157
175
|
else:
|
|
158
176
|
r = tools.run_command(self.root, cmd)
|
|
177
|
+
# Loop guard: if this exact command already failed, tell the model
|
|
178
|
+
# to change approach instead of repeating it.
|
|
179
|
+
if not r.ok:
|
|
180
|
+
n = self._failed_cmds.get(cmd, 0) + 1
|
|
181
|
+
self._failed_cmds[cmd] = n
|
|
182
|
+
if n >= 2:
|
|
183
|
+
r = ToolResult(False, r.output + (
|
|
184
|
+
"\n\nNOTE: this exact command has now failed "
|
|
185
|
+
f"{n} times. Do NOT run it again. Change approach — e.g. use the "
|
|
186
|
+
"make_dir/write_file tools, or fix quoting (wrap paths with spaces "
|
|
187
|
+
"in double quotes)."), display=r.display)
|
|
159
188
|
else:
|
|
160
189
|
r = ToolResult(False, f"Unknown tool: {tool}")
|
|
161
190
|
|
|
@@ -146,6 +146,19 @@ def commit_write(root: Path, path: str, new_text: str) -> bool:
|
|
|
146
146
|
return True
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def make_dir(root: Path, path: str) -> ToolResult:
|
|
150
|
+
"""Create a directory (and parents) without the shell — avoids cross-platform
|
|
151
|
+
quoting pitfalls (e.g. spaces in Windows paths)."""
|
|
152
|
+
dp = _resolve(root, path)
|
|
153
|
+
if dp is None:
|
|
154
|
+
return ToolResult(False, f"Refused: '{path}' is outside the project.")
|
|
155
|
+
try:
|
|
156
|
+
dp.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
except OSError as e:
|
|
158
|
+
return ToolResult(False, f"Cannot create {path}: {e}")
|
|
159
|
+
return ToolResult(True, f"created directory {path}", display=f"mkdir {path}")
|
|
160
|
+
|
|
161
|
+
|
|
149
162
|
def run_command(root: Path, command: str, timeout: float = 120.0) -> ToolResult:
|
|
150
163
|
try:
|
|
151
164
|
proc = subprocess.run(
|
|
@@ -60,6 +60,21 @@ def test_run_command(tmp_path):
|
|
|
60
60
|
assert r.ok and "weio-ok" in r.output
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def test_make_dir_with_spaces(tmp_path):
|
|
64
|
+
r = tools.make_dir(tmp_path, "My Project Folder")
|
|
65
|
+
assert r.ok and (tmp_path / "My Project Folder").is_dir()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_make_dir_nested(tmp_path):
|
|
69
|
+
r = tools.make_dir(tmp_path, "a/b/c")
|
|
70
|
+
assert r.ok and (tmp_path / "a" / "b" / "c").is_dir()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_make_dir_confined(tmp_path):
|
|
74
|
+
r = tools.make_dir(tmp_path, "../escape")
|
|
75
|
+
assert not r.ok and "outside" in r.output.lower()
|
|
76
|
+
|
|
77
|
+
|
|
63
78
|
# ---- action parsing ----
|
|
64
79
|
def test_parse_single_action():
|
|
65
80
|
text = 'I will read it.\n```action\n{"tool": "read_file", "args": {"path": "a.py"}}\n```'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|