weio-cli 0.4.0__tar.gz → 0.5.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.5.0}/PKG-INFO +51 -26
  2. {weio_cli-0.4.0 → weio_cli-0.5.0}/README.md +50 -25
  3. {weio_cli-0.4.0 → weio_cli-0.5.0}/pyproject.toml +1 -1
  4. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli/__init__.py +1 -1
  5. weio_cli-0.5.0/src/weio_cli/agent.py +237 -0
  6. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli/cli.py +34 -62
  7. weio_cli-0.5.0/src/weio_cli/settings.py +69 -0
  8. weio_cli-0.5.0/src/weio_cli/tools.py +167 -0
  9. weio_cli-0.5.0/src/weio_cli/tui.py +345 -0
  10. {weio_cli-0.4.0 → weio_cli-0.5.0/src/weio_cli.egg-info}/PKG-INFO +51 -26
  11. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli.egg-info/SOURCES.txt +4 -2
  12. weio_cli-0.5.0/tests/test_agent.py +90 -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.5.0}/LICENSE +0 -0
  17. {weio_cli-0.4.0 → weio_cli-0.5.0}/setup.cfg +0 -0
  18. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli/__main__.py +0 -0
  19. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli/browser_login.py +0 -0
  20. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli/client.py +0 -0
  21. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli/config.py +0 -0
  22. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli/updater.py +0 -0
  23. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli.egg-info/dependency_links.txt +0 -0
  24. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli.egg-info/entry_points.txt +0 -0
  25. {weio_cli-0.4.0 → weio_cli-0.5.0}/src/weio_cli.egg-info/requires.txt +0 -0
  26. {weio_cli-0.4.0 → weio_cli-0.5.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.5.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.5.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.5.0"
@@ -0,0 +1,237 @@
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 re
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Callable, Optional
16
+
17
+ from . import tools
18
+ from .client import WeioClient, WeioError
19
+ from .tools import ToolResult
20
+
21
+ _FENCE_RE = re.compile(r"```(?:action|json)?\s*\n(.*?)```", re.DOTALL)
22
+ _BARE_RE = re.compile(r'\{[^{}]*"tool"\s*:\s*"[a-z_]+".*?\}', re.DOTALL)
23
+
24
+ READONLY_TOOLS = {"read_file", "list_dir", "search"}
25
+ WRITE_TOOLS = {"write_file", "edit_file"}
26
+ ALL_TOOLS = READONLY_TOOLS | WRITE_TOOLS | {"run_command", "finish"}
27
+
28
+
29
+ def system_prompt(root: Path) -> str:
30
+ return f"""You are Weio, an autonomous coding agent working in the user's project directory:
31
+ {root}
32
+
33
+ You complete the user's task by using TOOLS, one step at a time. On each turn, briefly
34
+ state what you are about to do (one or two sentences), then emit one or more tool calls
35
+ as fenced JSON blocks exactly like this:
36
+
37
+ ```action
38
+ {{"tool": "read_file", "args": {{"path": "src/app.py"}}}}
39
+ ```
40
+
41
+ TOOLS:
42
+ - read_file {{"path": "..."}} read a file
43
+ - list_dir {{"path": "."}} list a directory
44
+ - search {{"query": "...", "path": "."}} search file contents
45
+ - write_file {{"path": "...", "content": "..."}} create/overwrite a file
46
+ - edit_file {{"path": "...", "search": "...", "replace": "..."}} replace an exact snippet
47
+ - run_command {{"command": "..."}} run a shell command in the project
48
+ - finish {{"summary": "..."}} call when the task is fully done
49
+
50
+ RULES:
51
+ - Explore before you edit: read_file / list_dir / search to understand the code first.
52
+ - Take ONE logical step, then STOP and wait for the OBSERVATIONS before the next step.
53
+ - edit_file "search" must match the current file contents EXACTLY (whitespace included).
54
+ - Prefer edit_file for small changes; write_file for new files or full rewrites.
55
+ - After making changes, verify (e.g. run tests or the file) when reasonable.
56
+ - When the task is complete, call finish with a short summary. Never call finish early.
57
+ - DO NOT just describe what you would do — emit the ```action block and DO it now.
58
+ - Every reply must contain exactly one ```action block (until you call finish).
59
+
60
+ EXAMPLE turn:
61
+ Let me look at the file before changing it.
62
+ ```action
63
+ {{"tool": "read_file", "args": {{"path": "math_utils.py"}}}}
64
+ ```"""
65
+
66
+
67
+ @dataclass
68
+ class Callbacks:
69
+ on_text: Callable[[str], None] # model narration
70
+ on_action: Callable[[str, dict], None] # about to run a tool
71
+ on_result: Callable[[str, ToolResult], None] # tool finished
72
+ approve: Callable[[str, dict, Optional[ToolResult]], bool] # ask user (writes/cmds)
73
+ on_status: Callable[[str], None] # spinner/status text
74
+ on_finish: Callable[[str], None] # final summary
75
+
76
+
77
+ def parse_actions(text: str) -> list[dict]:
78
+ """Extract tool calls. Accepts ```action / ```json fences, and bare JSON
79
+ objects containing a "tool" key (robust to weaker models' formatting)."""
80
+ actions = []
81
+ seen = set()
82
+ for m in _FENCE_RE.finditer(text):
83
+ raw = m.group(1).strip()
84
+ try:
85
+ obj = json.loads(raw)
86
+ except json.JSONDecodeError:
87
+ continue
88
+ if isinstance(obj, dict) and "tool" in obj:
89
+ key = json.dumps(obj, sort_keys=True)
90
+ if key not in seen:
91
+ seen.add(key)
92
+ actions.append(obj)
93
+ if not actions:
94
+ for m in _BARE_RE.finditer(text):
95
+ try:
96
+ obj = json.loads(m.group(0))
97
+ except json.JSONDecodeError:
98
+ continue
99
+ if isinstance(obj, dict) and "tool" in obj:
100
+ key = json.dumps(obj, sort_keys=True)
101
+ if key not in seen:
102
+ seen.add(key)
103
+ actions.append(obj)
104
+ return actions
105
+
106
+
107
+ def strip_actions(text: str) -> str:
108
+ return _BARE_RE.sub("", _FENCE_RE.sub("", text)).strip()
109
+
110
+
111
+ class Agent:
112
+ def __init__(self, client: WeioClient, root: Path, settings: dict, cb: Callbacks):
113
+ self.client = client
114
+ self.root = root
115
+ self.settings = settings
116
+ self.cb = cb
117
+ self.messages: list[dict] = [{"role": "system", "content": system_prompt(root)}]
118
+ self.approval = settings.get("approval", "suggest")
119
+ self.max_steps = int(settings.get("max_steps", 25))
120
+ self.max_tokens = int(settings.get("max_tokens", 4096))
121
+
122
+ # -- model call --
123
+ def _complete(self) -> str:
124
+ return self.client.chat(self.messages, max_tokens=self.max_tokens, temperature=0.1)
125
+
126
+ # -- approval policy --
127
+ def _needs_approval(self, tool: str) -> bool:
128
+ if self.approval == "full-auto":
129
+ return False
130
+ if tool in WRITE_TOOLS:
131
+ return self.approval == "suggest"
132
+ if tool == "run_command":
133
+ return True # commands always confirmed unless full-auto
134
+ return False
135
+
136
+ # -- execute one tool, return observation string --
137
+ def _execute(self, tool: str, args: dict) -> str:
138
+ self.cb.on_action(tool, args)
139
+
140
+ if tool == "read_file":
141
+ r = tools.read_file(self.root, args.get("path", ""))
142
+ elif tool == "list_dir":
143
+ r = tools.list_dir(self.root, args.get("path", "."))
144
+ elif tool == "search":
145
+ r = tools.search_files(self.root, args.get("query", ""), args.get("path", "."))
146
+ elif tool == "write_file":
147
+ r = tools.write_file(self.root, args.get("path", ""), args.get("content", ""))
148
+ r = self._maybe_commit(tool, args, r)
149
+ elif tool == "edit_file":
150
+ r = tools.edit_file(self.root, args.get("path", ""),
151
+ args.get("search", ""), args.get("replace", ""))
152
+ r = self._maybe_commit(tool, args, r)
153
+ elif tool == "run_command":
154
+ cmd = args.get("command", "")
155
+ if self._needs_approval(tool) and not self.cb.approve(tool, args, None):
156
+ r = ToolResult(False, "Command declined by user.", display=f"$ {cmd} (declined)")
157
+ else:
158
+ r = tools.run_command(self.root, cmd)
159
+ else:
160
+ r = ToolResult(False, f"Unknown tool: {tool}")
161
+
162
+ self.cb.on_result(tool, r)
163
+ return f"[{tool} {args.get('path') or args.get('command') or args.get('query') or ''}]\n{r.output}"
164
+
165
+ def _maybe_commit(self, tool: str, args: dict, r: ToolResult) -> ToolResult:
166
+ if not r.ok:
167
+ return r
168
+ if self._needs_approval(tool) and not self.cb.approve(tool, args, r):
169
+ return ToolResult(False, f"{tool} on {r.path} declined by user.",
170
+ display=f"{tool} {r.path} (declined)")
171
+ if tools.commit_write(self.root, r.path, r.new_text):
172
+ return r
173
+ return ToolResult(False, f"Failed to write {r.path}")
174
+
175
+ # -- main loop --
176
+ _CLAIM_WORDS = ("added", "created", "edited", "wrote", "updated", "modified",
177
+ "implemented", "changed", "inserted", "removed", "deleted")
178
+
179
+ def run_task(self, task: str) -> str:
180
+ self.messages.append({"role": "user", "content": task})
181
+ nudges = 0
182
+ for step in range(self.max_steps):
183
+ self.cb.on_status("thinking")
184
+ try:
185
+ response = self._complete()
186
+ except WeioError as e:
187
+ self.cb.on_text(f"[error] {e}")
188
+ return f"error: {e}"
189
+
190
+ narration = strip_actions(response)
191
+ if narration:
192
+ self.cb.on_text(narration)
193
+
194
+ actions = parse_actions(response)
195
+ self.messages.append({"role": "assistant", "content": response})
196
+
197
+ if not actions:
198
+ # The model replied without a tool call. If it sounds like it
199
+ # claimed to make a change (but didn't actually act), push back
200
+ # hard. Allow up to 2 nudges before accepting the reply as final.
201
+ claims_edit = any(w in narration.lower() for w in self._CLAIM_WORDS)
202
+ if nudges < 2:
203
+ nudges += 1
204
+ msg = ("You did NOT emit a tool call — nothing changed on disk. "
205
+ "Nothing happens until you emit a ```action block. "
206
+ "If you intended to edit a file, emit the edit_file (or write_file) "
207
+ "action now with the exact content. Otherwise call finish.")
208
+ if not claims_edit:
209
+ msg = ("Take the next step now as a ```action block, "
210
+ "or call finish if the task is already complete.")
211
+ self.messages.append({"role": "user", "content": msg})
212
+ continue
213
+ self.cb.on_finish(narration or "(done)")
214
+ return narration
215
+ nudges = 0
216
+
217
+ observations = []
218
+ finished = None
219
+ for act in actions:
220
+ tool = act.get("tool")
221
+ args = act.get("args", {}) if isinstance(act.get("args"), dict) else {}
222
+ if tool == "finish":
223
+ finished = args.get("summary", "Done.")
224
+ break
225
+ if tool not in ALL_TOOLS:
226
+ observations.append(f"[{tool}] Unknown tool. Use only the listed tools.")
227
+ continue
228
+ observations.append(self._execute(tool, args))
229
+
230
+ if finished is not None:
231
+ self.cb.on_finish(finished)
232
+ return finished
233
+
234
+ self.messages.append({"role": "user", "content": "OBSERVATIONS:\n" + "\n\n".join(observations)})
235
+
236
+ self.cb.on_finish("Reached the step limit before finishing. Re-run to continue.")
237
+ 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"]
@@ -0,0 +1,69 @@
1
+ """Persistent CLI settings (~/.weio/settings.json).
2
+
3
+ Separate from config.py (which holds the API key/base). These are behavior
4
+ preferences for the coding agent.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+
12
+ APPROVAL_MODES = ("suggest", "auto-edit", "full-auto")
13
+
14
+ DEFAULTS = {
15
+ "model": "auto", # auto | low | mid | pro
16
+ "approval": "suggest", # suggest | auto-edit | full-auto
17
+ "max_steps": 25, # max agent tool-steps per task
18
+ "max_tokens": 4096,
19
+ "stream": True,
20
+ }
21
+
22
+ _VALID_KEYS = set(DEFAULTS)
23
+
24
+
25
+ def _dir() -> Path:
26
+ return Path(os.environ.get("WEIO_CONFIG_DIR", str(Path.home() / ".weio")))
27
+
28
+
29
+ def _file() -> Path:
30
+ return _dir() / "settings.json"
31
+
32
+
33
+ def load() -> dict:
34
+ data = dict(DEFAULTS)
35
+ try:
36
+ if _file().exists():
37
+ data.update({k: v for k, v in json.loads(_file().read_text()).items() if k in _VALID_KEYS})
38
+ except Exception:
39
+ pass
40
+ return data
41
+
42
+
43
+ def save(data: dict) -> Path:
44
+ _dir().mkdir(parents=True, exist_ok=True)
45
+ clean = {k: v for k, v in data.items() if k in _VALID_KEYS}
46
+ _file().write_text(json.dumps(clean, indent=2))
47
+ return _file()
48
+
49
+
50
+ def set_value(key: str, value: str) -> tuple[bool, str]:
51
+ """Set one setting from a string value. Returns (ok, message)."""
52
+ if key not in _VALID_KEYS:
53
+ return False, f"Unknown setting '{key}'. Valid: {', '.join(sorted(_VALID_KEYS))}"
54
+ data = load()
55
+ if key == "approval":
56
+ if value not in APPROVAL_MODES:
57
+ return False, f"approval must be one of: {', '.join(APPROVAL_MODES)}"
58
+ data[key] = value
59
+ elif key in ("max_steps", "max_tokens"):
60
+ try:
61
+ data[key] = max(1, int(value))
62
+ except ValueError:
63
+ return False, f"{key} must be an integer"
64
+ elif key == "stream":
65
+ data[key] = value.lower() in ("1", "true", "yes", "on")
66
+ else: # model
67
+ data[key] = value
68
+ save(data)
69
+ return True, f"{key} = {data[key]}"