weio-cli 0.4.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.4.0/src/weio_cli.egg-info → weio_cli-0.6.0}/PKG-INFO +51 -26
- {weio_cli-0.4.0 → weio_cli-0.6.0}/README.md +50 -25
- {weio_cli-0.4.0 → weio_cli-0.6.0}/pyproject.toml +1 -1
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/__init__.py +1 -1
- weio_cli-0.6.0/src/weio_cli/agent.py +266 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/cli.py +34 -62
- weio_cli-0.6.0/src/weio_cli/settings.py +69 -0
- weio_cli-0.6.0/src/weio_cli/tools.py +180 -0
- weio_cli-0.6.0/src/weio_cli/tui.py +345 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0/src/weio_cli.egg-info}/PKG-INFO +51 -26
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/SOURCES.txt +4 -2
- weio_cli-0.6.0/tests/test_agent.py +105 -0
- weio_cli-0.4.0/src/weio_cli/coder.py +0 -160
- weio_cli-0.4.0/src/weio_cli/tui.py +0 -328
- weio_cli-0.4.0/tests/test_coder.py +0 -87
- {weio_cli-0.4.0 → weio_cli-0.6.0}/LICENSE +0 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/setup.cfg +0 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/__main__.py +0 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/browser_login.py +0 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/client.py +0 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/config.py +0 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/updater.py +0 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/dependency_links.txt +0 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/entry_points.txt +0 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/requires.txt +0 -0
- {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weio-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Weio — an agentic coding assistant that routes inference through your Weio account.
|
|
5
5
|
Author: We I/O Labs
|
|
6
6
|
License: MIT
|
|
@@ -60,10 +60,11 @@ weio usage # tier, tokens used today, remaining, reset time
|
|
|
60
60
|
|
|
61
61
|
## Use
|
|
62
62
|
|
|
63
|
-
### Interactive
|
|
63
|
+
### Interactive agent (default)
|
|
64
64
|
|
|
65
|
-
Run `weio` with no arguments to
|
|
66
|
-
|
|
65
|
+
Run `weio` with no arguments to start an autonomous coding agent in your terminal —
|
|
66
|
+
like Claude Code / Codex. It reads files, searches, edits, and runs commands to
|
|
67
|
+
complete your task, showing each step:
|
|
67
68
|
|
|
68
69
|
```bash
|
|
69
70
|
cd my-project
|
|
@@ -71,38 +72,62 @@ weio
|
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
```
|
|
74
|
-
weio › add a /health route to app.py
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
weio › add a /health route to app.py and run the tests
|
|
76
|
+
|
|
77
|
+
● Let me look at the app first.
|
|
78
|
+
📖 read_file app.py
|
|
79
|
+
● Adding the route.
|
|
80
|
+
✎ edit_file app.py
|
|
81
|
+
│ + @app.get("/health")
|
|
82
|
+
│ + def health(): return {"ok": True}
|
|
83
|
+
apply edit_file to app.py? [y/N] y
|
|
84
|
+
● Verifying.
|
|
85
|
+
❯ run_command pytest -q
|
|
86
|
+
│ exit=0 … 5 passed
|
|
87
|
+
✔ Added /health and confirmed tests pass.
|
|
83
88
|
```
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
The agent uses tools: **read_file, list_dir, search, edit_file, write_file,
|
|
91
|
+
run_command**. File paths are confined to the project directory.
|
|
92
|
+
|
|
93
|
+
Slash commands: `/model <id>`, `/approval <mode>`, `/steps <n>`, `/settings`,
|
|
94
|
+
`/undo`, `/clear`, `/cwd`, `/help`, `/exit`.
|
|
95
|
+
|
|
96
|
+
### Approval modes
|
|
97
|
+
|
|
98
|
+
Control how much the agent does without asking (`/approval` or `weio config approval`):
|
|
99
|
+
|
|
100
|
+
| Mode | Edits | Shell commands |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `suggest` (default) | ask each time | ask each time |
|
|
103
|
+
| `auto-edit` | auto-apply | ask each time |
|
|
104
|
+
| `full-auto` | auto-apply | auto-run |
|
|
105
|
+
|
|
106
|
+
### Settings
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
weio config # show all settings
|
|
110
|
+
weio config approval auto-edit # set a value
|
|
111
|
+
weio config model pro # auto | low | mid | pro
|
|
112
|
+
weio config max_steps 40
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Settings live in `~/.weio/settings.json`. The agent uses the most capable model
|
|
116
|
+
your plan allows.
|
|
88
117
|
|
|
89
118
|
### One-shot & other commands
|
|
90
119
|
|
|
91
120
|
```bash
|
|
92
|
-
# Run a
|
|
121
|
+
# Run a coding task non-interactively (auto-applies edits; add --auto to also run commands):
|
|
93
122
|
weio "add error handling to the fetch() in api.py"
|
|
123
|
+
weio code "refactor db.py to async" --auto
|
|
94
124
|
|
|
95
|
-
#
|
|
96
|
-
weio code "refactor to async" -f server.py -f db.py
|
|
97
|
-
|
|
98
|
-
# One-shot question (no file edits):
|
|
125
|
+
# Quick question (no file edits):
|
|
99
126
|
weio ask "what does a 502 from nginx usually mean?"
|
|
100
127
|
|
|
101
|
-
#
|
|
102
|
-
weio
|
|
103
|
-
|
|
104
|
-
# Check connectivity and your key:
|
|
105
|
-
weio ping
|
|
128
|
+
weio chat # plain interactive chat
|
|
129
|
+
weio usage # tokens used / limits
|
|
130
|
+
weio ping # connectivity + key check
|
|
106
131
|
```
|
|
107
132
|
|
|
108
133
|
Edits are shown as a diff and require confirmation before anything is written
|
|
@@ -38,10 +38,11 @@ weio usage # tier, tokens used today, remaining, reset time
|
|
|
38
38
|
|
|
39
39
|
## Use
|
|
40
40
|
|
|
41
|
-
### Interactive
|
|
41
|
+
### Interactive agent (default)
|
|
42
42
|
|
|
43
|
-
Run `weio` with no arguments to
|
|
44
|
-
|
|
43
|
+
Run `weio` with no arguments to start an autonomous coding agent in your terminal —
|
|
44
|
+
like Claude Code / Codex. It reads files, searches, edits, and runs commands to
|
|
45
|
+
complete your task, showing each step:
|
|
45
46
|
|
|
46
47
|
```bash
|
|
47
48
|
cd my-project
|
|
@@ -49,38 +50,62 @@ weio
|
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
```
|
|
52
|
-
weio › add a /health route to app.py
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
weio › add a /health route to app.py and run the tests
|
|
54
|
+
|
|
55
|
+
● Let me look at the app first.
|
|
56
|
+
📖 read_file app.py
|
|
57
|
+
● Adding the route.
|
|
58
|
+
✎ edit_file app.py
|
|
59
|
+
│ + @app.get("/health")
|
|
60
|
+
│ + def health(): return {"ok": True}
|
|
61
|
+
apply edit_file to app.py? [y/N] y
|
|
62
|
+
● Verifying.
|
|
63
|
+
❯ run_command pytest -q
|
|
64
|
+
│ exit=0 … 5 passed
|
|
65
|
+
✔ Added /health and confirmed tests pass.
|
|
61
66
|
```
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
The agent uses tools: **read_file, list_dir, search, edit_file, write_file,
|
|
69
|
+
run_command**. File paths are confined to the project directory.
|
|
70
|
+
|
|
71
|
+
Slash commands: `/model <id>`, `/approval <mode>`, `/steps <n>`, `/settings`,
|
|
72
|
+
`/undo`, `/clear`, `/cwd`, `/help`, `/exit`.
|
|
73
|
+
|
|
74
|
+
### Approval modes
|
|
75
|
+
|
|
76
|
+
Control how much the agent does without asking (`/approval` or `weio config approval`):
|
|
77
|
+
|
|
78
|
+
| Mode | Edits | Shell commands |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `suggest` (default) | ask each time | ask each time |
|
|
81
|
+
| `auto-edit` | auto-apply | ask each time |
|
|
82
|
+
| `full-auto` | auto-apply | auto-run |
|
|
83
|
+
|
|
84
|
+
### Settings
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
weio config # show all settings
|
|
88
|
+
weio config approval auto-edit # set a value
|
|
89
|
+
weio config model pro # auto | low | mid | pro
|
|
90
|
+
weio config max_steps 40
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Settings live in `~/.weio/settings.json`. The agent uses the most capable model
|
|
94
|
+
your plan allows.
|
|
66
95
|
|
|
67
96
|
### One-shot & other commands
|
|
68
97
|
|
|
69
98
|
```bash
|
|
70
|
-
# Run a
|
|
99
|
+
# Run a coding task non-interactively (auto-applies edits; add --auto to also run commands):
|
|
71
100
|
weio "add error handling to the fetch() in api.py"
|
|
101
|
+
weio code "refactor db.py to async" --auto
|
|
72
102
|
|
|
73
|
-
#
|
|
74
|
-
weio code "refactor to async" -f server.py -f db.py
|
|
75
|
-
|
|
76
|
-
# One-shot question (no file edits):
|
|
103
|
+
# Quick question (no file edits):
|
|
77
104
|
weio ask "what does a 502 from nginx usually mean?"
|
|
78
105
|
|
|
79
|
-
#
|
|
80
|
-
weio
|
|
81
|
-
|
|
82
|
-
# Check connectivity and your key:
|
|
83
|
-
weio ping
|
|
106
|
+
weio chat # plain interactive chat
|
|
107
|
+
weio usage # tokens used / limits
|
|
108
|
+
weio ping # connectivity + key check
|
|
84
109
|
```
|
|
85
110
|
|
|
86
111
|
Edits are shown as a diff and require confirmation before anything is written
|
|
@@ -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"
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""The Weio coding agent: an autonomous tool-use loop, like Claude Code / Codex.
|
|
2
|
+
|
|
3
|
+
The model works step by step, emitting tool calls as fenced JSON ```action blocks.
|
|
4
|
+
The agent executes them (gated by the approval mode), feeds back observations, and
|
|
5
|
+
loops until the model calls `finish` or the step budget is exhausted.
|
|
6
|
+
|
|
7
|
+
UI is injected via callbacks so the TUI and one-shot runner share this engine.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import platform
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Callable, Optional
|
|
17
|
+
|
|
18
|
+
from . import tools
|
|
19
|
+
from .client import WeioClient, WeioError
|
|
20
|
+
from .tools import ToolResult
|
|
21
|
+
|
|
22
|
+
_FENCE_RE = re.compile(r"```(?:action|json)?\s*\n(.*?)```", re.DOTALL)
|
|
23
|
+
_BARE_RE = re.compile(r'\{[^{}]*"tool"\s*:\s*"[a-z_]+".*?\}', re.DOTALL)
|
|
24
|
+
|
|
25
|
+
READONLY_TOOLS = {"read_file", "list_dir", "search"}
|
|
26
|
+
WRITE_TOOLS = {"write_file", "edit_file"}
|
|
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.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def system_prompt(root: Path) -> str:
|
|
42
|
+
return f"""You are Weio, an autonomous coding agent working in the user's project directory:
|
|
43
|
+
{root}
|
|
44
|
+
|
|
45
|
+
Environment: {_os_line()}
|
|
46
|
+
|
|
47
|
+
You complete the user's task by using TOOLS, one step at a time. On each turn, briefly
|
|
48
|
+
state what you are about to do (one or two sentences), then emit one or more tool calls
|
|
49
|
+
as fenced JSON blocks exactly like this:
|
|
50
|
+
|
|
51
|
+
```action
|
|
52
|
+
{{"tool": "read_file", "args": {{"path": "src/app.py"}}}}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
TOOLS:
|
|
56
|
+
- read_file {{"path": "..."}} read a file
|
|
57
|
+
- list_dir {{"path": "."}} list a directory
|
|
58
|
+
- search {{"query": "...", "path": "."}} search file contents
|
|
59
|
+
- make_dir {{"path": "..."}} create a directory (handles spaces safely)
|
|
60
|
+
- write_file {{"path": "...", "content": "..."}} create/overwrite a file
|
|
61
|
+
- edit_file {{"path": "...", "search": "...", "replace": "..."}} replace an exact snippet
|
|
62
|
+
- run_command {{"command": "..."}} run a shell command in the project
|
|
63
|
+
- finish {{"summary": "..."}} call when the task is fully done
|
|
64
|
+
|
|
65
|
+
RULES:
|
|
66
|
+
- Explore before you edit: read_file / list_dir / search to understand the code first.
|
|
67
|
+
- Take ONE logical step, then STOP and wait for the OBSERVATIONS before the next step.
|
|
68
|
+
- edit_file "search" must match the current file contents EXACTLY (whitespace included).
|
|
69
|
+
- Prefer edit_file for small changes; write_file for new files or full rewrites.
|
|
70
|
+
- After making changes, verify (e.g. run tests or the file) when reasonable.
|
|
71
|
+
- When the task is complete, call finish with a short summary. Never call finish early.
|
|
72
|
+
- DO NOT just describe what you would do — emit the ```action block and DO it now.
|
|
73
|
+
- Every reply must contain exactly one ```action block (until you call finish).
|
|
74
|
+
|
|
75
|
+
EXAMPLE turn:
|
|
76
|
+
Let me look at the file before changing it.
|
|
77
|
+
```action
|
|
78
|
+
{{"tool": "read_file", "args": {{"path": "math_utils.py"}}}}
|
|
79
|
+
```"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class Callbacks:
|
|
84
|
+
on_text: Callable[[str], None] # model narration
|
|
85
|
+
on_action: Callable[[str, dict], None] # about to run a tool
|
|
86
|
+
on_result: Callable[[str, ToolResult], None] # tool finished
|
|
87
|
+
approve: Callable[[str, dict, Optional[ToolResult]], bool] # ask user (writes/cmds)
|
|
88
|
+
on_status: Callable[[str], None] # spinner/status text
|
|
89
|
+
on_finish: Callable[[str], None] # final summary
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def parse_actions(text: str) -> list[dict]:
|
|
93
|
+
"""Extract tool calls. Accepts ```action / ```json fences, and bare JSON
|
|
94
|
+
objects containing a "tool" key (robust to weaker models' formatting)."""
|
|
95
|
+
actions = []
|
|
96
|
+
seen = set()
|
|
97
|
+
for m in _FENCE_RE.finditer(text):
|
|
98
|
+
raw = m.group(1).strip()
|
|
99
|
+
try:
|
|
100
|
+
obj = json.loads(raw)
|
|
101
|
+
except json.JSONDecodeError:
|
|
102
|
+
continue
|
|
103
|
+
if isinstance(obj, dict) and "tool" in obj:
|
|
104
|
+
key = json.dumps(obj, sort_keys=True)
|
|
105
|
+
if key not in seen:
|
|
106
|
+
seen.add(key)
|
|
107
|
+
actions.append(obj)
|
|
108
|
+
if not actions:
|
|
109
|
+
for m in _BARE_RE.finditer(text):
|
|
110
|
+
try:
|
|
111
|
+
obj = json.loads(m.group(0))
|
|
112
|
+
except json.JSONDecodeError:
|
|
113
|
+
continue
|
|
114
|
+
if isinstance(obj, dict) and "tool" in obj:
|
|
115
|
+
key = json.dumps(obj, sort_keys=True)
|
|
116
|
+
if key not in seen:
|
|
117
|
+
seen.add(key)
|
|
118
|
+
actions.append(obj)
|
|
119
|
+
return actions
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def strip_actions(text: str) -> str:
|
|
123
|
+
return _BARE_RE.sub("", _FENCE_RE.sub("", text)).strip()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Agent:
|
|
127
|
+
def __init__(self, client: WeioClient, root: Path, settings: dict, cb: Callbacks):
|
|
128
|
+
self.client = client
|
|
129
|
+
self.root = root
|
|
130
|
+
self.settings = settings
|
|
131
|
+
self.cb = cb
|
|
132
|
+
self.messages: list[dict] = [{"role": "system", "content": system_prompt(root)}]
|
|
133
|
+
self.approval = settings.get("approval", "suggest")
|
|
134
|
+
self.max_steps = int(settings.get("max_steps", 25))
|
|
135
|
+
self.max_tokens = int(settings.get("max_tokens", 4096))
|
|
136
|
+
self._failed_cmds: dict[str, int] = {}
|
|
137
|
+
|
|
138
|
+
# -- model call --
|
|
139
|
+
def _complete(self) -> str:
|
|
140
|
+
return self.client.chat(self.messages, max_tokens=self.max_tokens, temperature=0.1)
|
|
141
|
+
|
|
142
|
+
# -- approval policy --
|
|
143
|
+
def _needs_approval(self, tool: str) -> bool:
|
|
144
|
+
if self.approval == "full-auto":
|
|
145
|
+
return False
|
|
146
|
+
if tool in WRITE_TOOLS:
|
|
147
|
+
return self.approval == "suggest"
|
|
148
|
+
if tool == "run_command":
|
|
149
|
+
return True # commands always confirmed unless full-auto
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
# -- execute one tool, return observation string --
|
|
153
|
+
def _execute(self, tool: str, args: dict) -> str:
|
|
154
|
+
self.cb.on_action(tool, args)
|
|
155
|
+
|
|
156
|
+
if tool == "read_file":
|
|
157
|
+
r = tools.read_file(self.root, args.get("path", ""))
|
|
158
|
+
elif tool == "list_dir":
|
|
159
|
+
r = tools.list_dir(self.root, args.get("path", "."))
|
|
160
|
+
elif tool == "search":
|
|
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", ""))
|
|
164
|
+
elif tool == "write_file":
|
|
165
|
+
r = tools.write_file(self.root, args.get("path", ""), args.get("content", ""))
|
|
166
|
+
r = self._maybe_commit(tool, args, r)
|
|
167
|
+
elif tool == "edit_file":
|
|
168
|
+
r = tools.edit_file(self.root, args.get("path", ""),
|
|
169
|
+
args.get("search", ""), args.get("replace", ""))
|
|
170
|
+
r = self._maybe_commit(tool, args, r)
|
|
171
|
+
elif tool == "run_command":
|
|
172
|
+
cmd = args.get("command", "")
|
|
173
|
+
if self._needs_approval(tool) and not self.cb.approve(tool, args, None):
|
|
174
|
+
r = ToolResult(False, "Command declined by user.", display=f"$ {cmd} (declined)")
|
|
175
|
+
else:
|
|
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)
|
|
188
|
+
else:
|
|
189
|
+
r = ToolResult(False, f"Unknown tool: {tool}")
|
|
190
|
+
|
|
191
|
+
self.cb.on_result(tool, r)
|
|
192
|
+
return f"[{tool} {args.get('path') or args.get('command') or args.get('query') or ''}]\n{r.output}"
|
|
193
|
+
|
|
194
|
+
def _maybe_commit(self, tool: str, args: dict, r: ToolResult) -> ToolResult:
|
|
195
|
+
if not r.ok:
|
|
196
|
+
return r
|
|
197
|
+
if self._needs_approval(tool) and not self.cb.approve(tool, args, r):
|
|
198
|
+
return ToolResult(False, f"{tool} on {r.path} declined by user.",
|
|
199
|
+
display=f"{tool} {r.path} (declined)")
|
|
200
|
+
if tools.commit_write(self.root, r.path, r.new_text):
|
|
201
|
+
return r
|
|
202
|
+
return ToolResult(False, f"Failed to write {r.path}")
|
|
203
|
+
|
|
204
|
+
# -- main loop --
|
|
205
|
+
_CLAIM_WORDS = ("added", "created", "edited", "wrote", "updated", "modified",
|
|
206
|
+
"implemented", "changed", "inserted", "removed", "deleted")
|
|
207
|
+
|
|
208
|
+
def run_task(self, task: str) -> str:
|
|
209
|
+
self.messages.append({"role": "user", "content": task})
|
|
210
|
+
nudges = 0
|
|
211
|
+
for step in range(self.max_steps):
|
|
212
|
+
self.cb.on_status("thinking")
|
|
213
|
+
try:
|
|
214
|
+
response = self._complete()
|
|
215
|
+
except WeioError as e:
|
|
216
|
+
self.cb.on_text(f"[error] {e}")
|
|
217
|
+
return f"error: {e}"
|
|
218
|
+
|
|
219
|
+
narration = strip_actions(response)
|
|
220
|
+
if narration:
|
|
221
|
+
self.cb.on_text(narration)
|
|
222
|
+
|
|
223
|
+
actions = parse_actions(response)
|
|
224
|
+
self.messages.append({"role": "assistant", "content": response})
|
|
225
|
+
|
|
226
|
+
if not actions:
|
|
227
|
+
# The model replied without a tool call. If it sounds like it
|
|
228
|
+
# claimed to make a change (but didn't actually act), push back
|
|
229
|
+
# hard. Allow up to 2 nudges before accepting the reply as final.
|
|
230
|
+
claims_edit = any(w in narration.lower() for w in self._CLAIM_WORDS)
|
|
231
|
+
if nudges < 2:
|
|
232
|
+
nudges += 1
|
|
233
|
+
msg = ("You did NOT emit a tool call — nothing changed on disk. "
|
|
234
|
+
"Nothing happens until you emit a ```action block. "
|
|
235
|
+
"If you intended to edit a file, emit the edit_file (or write_file) "
|
|
236
|
+
"action now with the exact content. Otherwise call finish.")
|
|
237
|
+
if not claims_edit:
|
|
238
|
+
msg = ("Take the next step now as a ```action block, "
|
|
239
|
+
"or call finish if the task is already complete.")
|
|
240
|
+
self.messages.append({"role": "user", "content": msg})
|
|
241
|
+
continue
|
|
242
|
+
self.cb.on_finish(narration or "(done)")
|
|
243
|
+
return narration
|
|
244
|
+
nudges = 0
|
|
245
|
+
|
|
246
|
+
observations = []
|
|
247
|
+
finished = None
|
|
248
|
+
for act in actions:
|
|
249
|
+
tool = act.get("tool")
|
|
250
|
+
args = act.get("args", {}) if isinstance(act.get("args"), dict) else {}
|
|
251
|
+
if tool == "finish":
|
|
252
|
+
finished = args.get("summary", "Done.")
|
|
253
|
+
break
|
|
254
|
+
if tool not in ALL_TOOLS:
|
|
255
|
+
observations.append(f"[{tool}] Unknown tool. Use only the listed tools.")
|
|
256
|
+
continue
|
|
257
|
+
observations.append(self._execute(tool, args))
|
|
258
|
+
|
|
259
|
+
if finished is not None:
|
|
260
|
+
self.cb.on_finish(finished)
|
|
261
|
+
return finished
|
|
262
|
+
|
|
263
|
+
self.messages.append({"role": "user", "content": "OBSERVATIONS:\n" + "\n\n".join(observations)})
|
|
264
|
+
|
|
265
|
+
self.cb.on_finish("Reached the step limit before finishing. Re-run to continue.")
|
|
266
|
+
return "step-limit"
|
|
@@ -16,7 +16,6 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
from . import __version__, config
|
|
18
18
|
from .client import WeioClient, WeioError
|
|
19
|
-
from . import coder
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
def _make_client(args) -> WeioClient:
|
|
@@ -198,74 +197,41 @@ def cmd_tui(args) -> int:
|
|
|
198
197
|
|
|
199
198
|
|
|
200
199
|
def cmd_code(args) -> int:
|
|
200
|
+
"""One-shot agent run: `weio "do X"` (non-interactive)."""
|
|
201
201
|
try:
|
|
202
202
|
client = _make_client(args)
|
|
203
203
|
except WeioError as e:
|
|
204
204
|
print(_c(str(e), "31"), file=sys.stderr)
|
|
205
205
|
return 2
|
|
206
|
-
root = Path(args.
|
|
206
|
+
root = Path(getattr(args, "dir", ".") or ".").resolve()
|
|
207
207
|
if not root.is_dir():
|
|
208
208
|
print(_c(f"Not a directory: {root}", "31"), file=sys.stderr)
|
|
209
209
|
return 2
|
|
210
|
+
# Non-interactive: auto-apply edits; run commands only in full-auto.
|
|
211
|
+
approval = "full-auto" if getattr(args, "auto", False) else "auto-edit"
|
|
212
|
+
from .tui import run_once
|
|
213
|
+
return run_once(client, root, args.message, approval=approval, max_tokens=_max_tokens(args))
|
|
210
214
|
|
|
211
|
-
print(_c(f"weio coding in {root}", "36"))
|
|
212
|
-
print("Thinking…", flush=True)
|
|
213
|
-
try:
|
|
214
|
-
raw, edits = coder.run_coding_task(
|
|
215
|
-
client, args.message, root=root, files=args.file or [], max_tokens=_max_tokens(args)
|
|
216
|
-
)
|
|
217
|
-
except WeioError as e:
|
|
218
|
-
print(_c(str(e), "31"), file=sys.stderr)
|
|
219
|
-
return 1
|
|
220
215
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
216
|
+
def cmd_config(args) -> int:
|
|
217
|
+
from . import settings as settings_mod
|
|
218
|
+
key = getattr(args, "key_", None)
|
|
219
|
+
value = getattr(args, "value", None)
|
|
220
|
+
if not key:
|
|
221
|
+
st = settings_mod.load()
|
|
222
|
+
print(_c("weio settings:", "1"))
|
|
223
|
+
for k in ("model", "approval", "max_steps", "max_tokens", "stream"):
|
|
224
|
+
print(f" {k:11} {st.get(k)}")
|
|
225
|
+
print(_c(f"\nfile: {settings_mod._file()}", "2"))
|
|
226
|
+
print(_c("set with: weio config <key> <value> (e.g. weio config approval auto-edit)", "2"))
|
|
224
227
|
return 0
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
elif res:
|
|
233
|
-
results.append(res)
|
|
234
|
-
|
|
235
|
-
for res in results:
|
|
236
|
-
tag = _c("NEW", "32") if res.created else _c("EDIT", "33")
|
|
237
|
-
print(f"\n{tag} {res.path}")
|
|
238
|
-
if res.created:
|
|
239
|
-
preview = res.new_text if len(res.new_text) < 1500 else res.new_text[:1500] + "\n… [truncated preview]"
|
|
240
|
-
print(preview)
|
|
241
|
-
else:
|
|
242
|
-
print(coder.unified_diff(res.path, res.old_text, res.new_text) or "(no textual diff)")
|
|
243
|
-
|
|
244
|
-
for err in errors:
|
|
245
|
-
print(_c("SKIP " + err, "31"))
|
|
246
|
-
|
|
247
|
-
if not results:
|
|
248
|
-
print(_c("\nNo applicable edits.", "33"))
|
|
249
|
-
return 1
|
|
250
|
-
|
|
251
|
-
if not args.yes:
|
|
252
|
-
try:
|
|
253
|
-
ans = input(_c(f"\nApply {len(results)} change(s)? [y/N] ", "36")).strip().lower()
|
|
254
|
-
except (EOFError, KeyboardInterrupt):
|
|
255
|
-
print()
|
|
256
|
-
ans = "n"
|
|
257
|
-
if ans not in ("y", "yes"):
|
|
258
|
-
print("Aborted. No files written.")
|
|
259
|
-
return 0
|
|
260
|
-
|
|
261
|
-
written = 0
|
|
262
|
-
for res in results:
|
|
263
|
-
fp = root / res.path
|
|
264
|
-
fp.parent.mkdir(parents=True, exist_ok=True)
|
|
265
|
-
fp.write_text(res.new_text, encoding="utf-8")
|
|
266
|
-
written += 1
|
|
267
|
-
print(_c(f"\nApplied {written} change(s).", "32"))
|
|
268
|
-
return 0
|
|
228
|
+
if value is None:
|
|
229
|
+
st = settings_mod.load()
|
|
230
|
+
print(f"{key} = {st.get(key)}")
|
|
231
|
+
return 0
|
|
232
|
+
ok, msg = settings_mod.set_value(key, value)
|
|
233
|
+
print(_c(msg, "32" if ok else "31"))
|
|
234
|
+
return 0 if ok else 1
|
|
269
235
|
|
|
270
236
|
|
|
271
237
|
# --------------------------------------------------------------------------- #
|
|
@@ -321,13 +287,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
321
287
|
sp = sub.add_parser("update", parents=[common], help="Update weio-cli to the latest version")
|
|
322
288
|
sp.set_defaults(func=cmd_update)
|
|
323
289
|
|
|
324
|
-
sp = sub.add_parser("code", parents=[common], help="Run
|
|
290
|
+
sp = sub.add_parser("code", parents=[common], help="Run an agent task non-interactively")
|
|
325
291
|
sp.add_argument("message")
|
|
326
|
-
sp.add_argument("--file", "-f", action="append", help="Add a file to the context (repeatable)")
|
|
327
292
|
sp.add_argument("--dir", "-C", default=".", help="Project directory (default: cwd)")
|
|
328
|
-
sp.add_argument("--
|
|
293
|
+
sp.add_argument("--auto", action="store_true",
|
|
294
|
+
help="Full-auto: also run shell commands without asking")
|
|
329
295
|
sp.set_defaults(func=cmd_code)
|
|
330
296
|
|
|
297
|
+
sp = sub.add_parser("config", parents=[common], help="View or change CLI settings")
|
|
298
|
+
sp.add_argument("key_", metavar="key", nargs="?",
|
|
299
|
+
help="Setting: model | approval | max_steps | max_tokens | stream")
|
|
300
|
+
sp.add_argument("value", nargs="?", help="New value")
|
|
301
|
+
sp.set_defaults(func=cmd_config)
|
|
302
|
+
|
|
331
303
|
return p
|
|
332
304
|
|
|
333
305
|
|
|
@@ -335,7 +307,7 @@ def main(argv=None) -> int:
|
|
|
335
307
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
336
308
|
parser = build_parser()
|
|
337
309
|
|
|
338
|
-
known = {"login", "ping", "ask", "chat", "code", "tui", "update", "usage"}
|
|
310
|
+
known = {"login", "ping", "ask", "chat", "code", "tui", "update", "usage", "config"}
|
|
339
311
|
# Bare `weio` (no args) → interactive coding TUI, like Claude Code.
|
|
340
312
|
if not argv:
|
|
341
313
|
argv = ["tui"]
|