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.
Files changed (26) hide show
  1. {weio_cli-0.4.0/src/weio_cli.egg-info → weio_cli-0.6.0}/PKG-INFO +51 -26
  2. {weio_cli-0.4.0 → weio_cli-0.6.0}/README.md +50 -25
  3. {weio_cli-0.4.0 → weio_cli-0.6.0}/pyproject.toml +1 -1
  4. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/__init__.py +1 -1
  5. weio_cli-0.6.0/src/weio_cli/agent.py +266 -0
  6. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/cli.py +34 -62
  7. weio_cli-0.6.0/src/weio_cli/settings.py +69 -0
  8. weio_cli-0.6.0/src/weio_cli/tools.py +180 -0
  9. weio_cli-0.6.0/src/weio_cli/tui.py +345 -0
  10. {weio_cli-0.4.0 → weio_cli-0.6.0/src/weio_cli.egg-info}/PKG-INFO +51 -26
  11. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/SOURCES.txt +4 -2
  12. weio_cli-0.6.0/tests/test_agent.py +105 -0
  13. weio_cli-0.4.0/src/weio_cli/coder.py +0 -160
  14. weio_cli-0.4.0/src/weio_cli/tui.py +0 -328
  15. weio_cli-0.4.0/tests/test_coder.py +0 -87
  16. {weio_cli-0.4.0 → weio_cli-0.6.0}/LICENSE +0 -0
  17. {weio_cli-0.4.0 → weio_cli-0.6.0}/setup.cfg +0 -0
  18. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/__main__.py +0 -0
  19. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/browser_login.py +0 -0
  20. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/client.py +0 -0
  21. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/config.py +0 -0
  22. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli/updater.py +0 -0
  23. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/dependency_links.txt +0 -0
  24. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/entry_points.txt +0 -0
  25. {weio_cli-0.4.0 → weio_cli-0.6.0}/src/weio_cli.egg-info/requires.txt +0 -0
  26. {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.4.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 TUI (default)
63
+ ### Interactive agent (default)
64
64
 
65
- Run `weio` with no arguments to drop into an interactive coding session like a
66
- local pair-programmer in your terminal:
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 that returns {"ok": true}
75
-
76
- weio: I'll add a health check route…
77
- edit app.py
78
- --- a/app.py
79
- +++ b/app.py
80
- @@
81
- Apply 1 change(s)? [y/N] y
82
- applied 1 change(s). (/undo to revert)
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
- Slash commands: `/add <file>`, `/drop <file>`, `/files`, `/auto` (auto-apply),
86
- `/model <id>`, `/undo`, `/clear`, `/cwd`, `/help`, `/exit`. Arrow-up recalls
87
- history. Files you mention or `/add` are kept in context and re-read each turn.
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 single coding task non-interactively (reads & edits files):
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
- # Add specific files to the context:
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
- # Interactive chat:
102
- weio chat
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 TUI (default)
41
+ ### Interactive agent (default)
42
42
 
43
- Run `weio` with no arguments to drop into an interactive coding session like a
44
- local pair-programmer in your terminal:
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 that returns {"ok": true}
53
-
54
- weio: I'll add a health check route…
55
- edit app.py
56
- --- a/app.py
57
- +++ b/app.py
58
- @@
59
- Apply 1 change(s)? [y/N] y
60
- applied 1 change(s). (/undo to revert)
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
- Slash commands: `/add <file>`, `/drop <file>`, `/files`, `/auto` (auto-apply),
64
- `/model <id>`, `/undo`, `/clear`, `/cwd`, `/help`, `/exit`. Arrow-up recalls
65
- history. Files you mention or `/add` are kept in context and re-read each turn.
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 single coding task non-interactively (reads & edits files):
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
- # Add specific files to the context:
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
- # Interactive chat:
80
- weio chat
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.4.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"
@@ -1,3 +1,3 @@
1
1
  """Weio CLI — an agentic coding assistant that routes inference through your Weio account."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.6.0"
@@ -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.dir).resolve()
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
- if not edits:
222
- # No edits show the model's reply (it may be asking for files or explaining)
223
- print(raw)
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
- # Apply edits (preview + confirm unless --yes)
227
- results, errors = [], []
228
- for e in edits:
229
- res, err = coder.apply_edit(e, root)
230
- if err:
231
- errors.append(err)
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 a coding task in a directory")
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("--yes", "-y", action="store_true", help="Apply edits without confirmation")
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"]