weio-cli 0.1.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weio-cli
3
- Version: 0.1.0
3
+ Version: 0.3.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
@@ -17,6 +17,7 @@ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: httpx>=0.24
20
+ Requires-Dist: prompt_toolkit>=3.0
20
21
  Dynamic: license-file
21
22
 
22
23
  # weio-cli
@@ -44,8 +45,36 @@ export WEIO_API_KEY="weio_sk_…"
44
45
 
45
46
  ## Use
46
47
 
48
+ ### Interactive TUI (default)
49
+
50
+ Run `weio` with no arguments to drop into an interactive coding session — like a
51
+ local pair-programmer in your terminal:
52
+
53
+ ```bash
54
+ cd my-project
55
+ weio
56
+ ```
57
+
58
+ ```
59
+ weio › add a /health route to app.py that returns {"ok": true}
60
+
61
+ weio: I'll add a health check route…
62
+ ● edit app.py
63
+ --- a/app.py
64
+ +++ b/app.py
65
+ @@ …
66
+ Apply 1 change(s)? [y/N] y
67
+ ✓ applied 1 change(s). (/undo to revert)
68
+ ```
69
+
70
+ Slash commands: `/add <file>`, `/drop <file>`, `/files`, `/auto` (auto-apply),
71
+ `/model <id>`, `/undo`, `/clear`, `/cwd`, `/help`, `/exit`. Arrow-up recalls
72
+ history. Files you mention or `/add` are kept in context and re-read each turn.
73
+
74
+ ### One-shot & other commands
75
+
47
76
  ```bash
48
- # Run a coding task in the current directory (reads & edits files):
77
+ # Run a single coding task non-interactively (reads & edits files):
49
78
  weio "add error handling to the fetch() in api.py"
50
79
 
51
80
  # Add specific files to the context:
@@ -64,6 +93,19 @@ weio ping
64
93
  Edits are shown as a diff and require confirmation before anything is written
65
94
  (use `-y`/`--yes` to apply automatically). New files are created as needed.
66
95
 
96
+ ## Updating
97
+
98
+ weio-cli checks PyPI once a day (fail-silent) and prints a one-line notice when a
99
+ newer version is available. To upgrade:
100
+
101
+ ```bash
102
+ weio update # upgrades in place via pip
103
+ # or
104
+ pip install -U weio-cli
105
+ ```
106
+
107
+ Disable the check with `WEIO_NO_UPDATE_CHECK=1`.
108
+
67
109
  ## Configuration
68
110
 
69
111
  | Setting | Flag | Env | Config file |
@@ -23,8 +23,36 @@ export WEIO_API_KEY="weio_sk_…"
23
23
 
24
24
  ## Use
25
25
 
26
+ ### Interactive TUI (default)
27
+
28
+ Run `weio` with no arguments to drop into an interactive coding session — like a
29
+ local pair-programmer in your terminal:
30
+
31
+ ```bash
32
+ cd my-project
33
+ weio
34
+ ```
35
+
36
+ ```
37
+ weio › add a /health route to app.py that returns {"ok": true}
38
+
39
+ weio: I'll add a health check route…
40
+ ● edit app.py
41
+ --- a/app.py
42
+ +++ b/app.py
43
+ @@ …
44
+ Apply 1 change(s)? [y/N] y
45
+ ✓ applied 1 change(s). (/undo to revert)
46
+ ```
47
+
48
+ Slash commands: `/add <file>`, `/drop <file>`, `/files`, `/auto` (auto-apply),
49
+ `/model <id>`, `/undo`, `/clear`, `/cwd`, `/help`, `/exit`. Arrow-up recalls
50
+ history. Files you mention or `/add` are kept in context and re-read each turn.
51
+
52
+ ### One-shot & other commands
53
+
26
54
  ```bash
27
- # Run a coding task in the current directory (reads & edits files):
55
+ # Run a single coding task non-interactively (reads & edits files):
28
56
  weio "add error handling to the fetch() in api.py"
29
57
 
30
58
  # Add specific files to the context:
@@ -43,6 +71,19 @@ weio ping
43
71
  Edits are shown as a diff and require confirmation before anything is written
44
72
  (use `-y`/`--yes` to apply automatically). New files are created as needed.
45
73
 
74
+ ## Updating
75
+
76
+ weio-cli checks PyPI once a day (fail-silent) and prints a one-line notice when a
77
+ newer version is available. To upgrade:
78
+
79
+ ```bash
80
+ weio update # upgrades in place via pip
81
+ # or
82
+ pip install -U weio-cli
83
+ ```
84
+
85
+ Disable the check with `WEIO_NO_UPDATE_CHECK=1`.
86
+
46
87
  ## Configuration
47
88
 
48
89
  | Setting | Flag | Env | Config file |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "weio-cli"
7
- version = "0.1.0"
7
+ version = "0.3.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"
@@ -19,7 +19,7 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3",
20
20
  "Topic :: Software Development :: Code Generators",
21
21
  ]
22
- dependencies = ["httpx>=0.24"]
22
+ dependencies = ["httpx>=0.24", "prompt_toolkit>=3.0"]
23
23
 
24
24
  [project.urls]
25
25
  Homepage = "https://weio.ai"
@@ -1,3 +1,3 @@
1
1
  """Weio CLI — an agentic coding assistant that routes inference through your Weio account."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.3.0"
@@ -128,6 +128,30 @@ def cmd_chat(args) -> int:
128
128
  return 0
129
129
 
130
130
 
131
+ def cmd_update(args) -> int:
132
+ from . import updater
133
+ latest = updater.check_for_update(force=True)
134
+ if not latest:
135
+ print(_c(f"weio-cli {__version__} is up to date.", "32"))
136
+ return 0
137
+ print(_c(f"Updating weio-cli {__version__} → {latest}…", "36"))
138
+ return updater.self_update()
139
+
140
+
141
+ def cmd_tui(args) -> int:
142
+ try:
143
+ client = _make_client(args)
144
+ except WeioError as e:
145
+ print(_c(str(e), "31"), file=sys.stderr)
146
+ return 2
147
+ root = Path(getattr(args, "dir", ".") or ".").resolve()
148
+ if not root.is_dir():
149
+ print(_c(f"Not a directory: {root}", "31"), file=sys.stderr)
150
+ return 2
151
+ from .tui import run_tui
152
+ return run_tui(client, root, max_tokens=_max_tokens(args))
153
+
154
+
131
155
  def cmd_code(args) -> int:
132
156
  try:
133
157
  client = _make_client(args)
@@ -239,6 +263,13 @@ def build_parser() -> argparse.ArgumentParser:
239
263
  sp = sub.add_parser("chat", parents=[common], help="Interactive chat")
240
264
  sp.set_defaults(func=cmd_chat)
241
265
 
266
+ sp = sub.add_parser("tui", parents=[common], help="Interactive coding TUI (default when run with no args)")
267
+ sp.add_argument("--dir", "-C", default=".", help="Project directory (default: cwd)")
268
+ sp.set_defaults(func=cmd_tui)
269
+
270
+ sp = sub.add_parser("update", parents=[common], help="Update weio-cli to the latest version")
271
+ sp.set_defaults(func=cmd_update)
272
+
242
273
  sp = sub.add_parser("code", parents=[common], help="Run a coding task in a directory")
243
274
  sp.add_argument("message")
244
275
  sp.add_argument("--file", "-f", action="append", help="Add a file to the context (repeatable)")
@@ -253,9 +284,12 @@ def main(argv=None) -> int:
253
284
  argv = list(sys.argv[1:] if argv is None else argv)
254
285
  parser = build_parser()
255
286
 
256
- # Bare `weio "do something"` defaults to the `code` command.
257
- known = {"login", "ping", "ask", "chat", "code"}
258
- if argv and argv[0] not in known and not argv[0].startswith("-"):
287
+ known = {"login", "ping", "ask", "chat", "code", "tui", "update"}
288
+ # Bare `weio` (no args) → interactive coding TUI, like Claude Code.
289
+ if not argv:
290
+ argv = ["tui"]
291
+ # Bare `weio "do something"` (a message, not a flag/command) → one-shot code.
292
+ elif argv[0] not in known and not argv[0].startswith("-"):
259
293
  argv = ["code", *argv]
260
294
 
261
295
  args = parser.parse_args(argv)
@@ -263,10 +297,21 @@ def main(argv=None) -> int:
263
297
  parser.print_help()
264
298
  return 1
265
299
  try:
266
- return args.func(args)
300
+ rc = args.func(args)
267
301
  except KeyboardInterrupt:
268
302
  print()
269
303
  return 130
304
+ # Throttled, fail-silent update notice (skipped for tui — it shows its own
305
+ # in the header — and for the update command itself).
306
+ if args.command not in ("update", "tui"):
307
+ try:
308
+ from . import updater
309
+ notice = updater.update_notice()
310
+ if notice:
311
+ print(_c("\n" + notice, "33"), file=sys.stderr)
312
+ except Exception:
313
+ pass
314
+ return rc
270
315
 
271
316
 
272
317
  if __name__ == "__main__":
@@ -73,6 +73,21 @@ class WeioClient:
73
73
  if r.status_code >= 400:
74
74
  body = r.read().decode("utf-8", "replace")
75
75
  raise WeioError(f"Gateway error {r.status_code}: {body[:300]}")
76
+
77
+ ctype = r.headers.get("content-type", "")
78
+ # Not all gateways honor stream=true. If the response isn't SSE,
79
+ # read the whole JSON body and yield its content in one shot.
80
+ if "text/event-stream" not in ctype:
81
+ body = r.read().decode("utf-8", "replace")
82
+ try:
83
+ content = json.loads(body)["choices"][0]["message"]["content"]
84
+ if content:
85
+ yield content
86
+ except (KeyError, IndexError, json.JSONDecodeError) as e:
87
+ raise WeioError(f"Unexpected response shape: {body[:300]}") from e
88
+ return
89
+
90
+ saw_delta = False
76
91
  for line in r.iter_lines():
77
92
  if not line or not line.startswith("data:"):
78
93
  continue
@@ -82,9 +97,14 @@ class WeioClient:
82
97
  try:
83
98
  delta = json.loads(data)["choices"][0].get("delta", {}).get("content")
84
99
  if delta:
100
+ saw_delta = True
85
101
  yield delta
86
102
  except (KeyError, IndexError, json.JSONDecodeError):
87
103
  continue
104
+ if not saw_delta:
105
+ # SSE content-type but no usable deltas — fall back to non-stream.
106
+ yield self.chat(messages, max_tokens=max_tokens,
107
+ temperature=temperature, model=model)
88
108
  except httpx.HTTPError as e:
89
109
  raise WeioError(f"Network error talking to {self.base}: {e}") from e
90
110
 
@@ -0,0 +1,328 @@
1
+ """Interactive coding TUI — a Claude Code-style REPL for weio.
2
+
3
+ Launch with bare `weio` (or `weio tui`). You get a persistent prompt where you
4
+ chat with the model; when it proposes file changes they're shown as a diff and
5
+ applied (with confirmation, or auto in /auto mode). Slash commands manage the
6
+ session. Uses prompt_toolkit when available, with a plain readline fallback.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from . import __version__, coder
15
+ from .client import WeioClient, WeioError
16
+
17
+ try:
18
+ from prompt_toolkit import PromptSession
19
+ from prompt_toolkit.history import FileHistory
20
+ from prompt_toolkit.completion import WordCompleter
21
+ from prompt_toolkit.styles import Style
22
+ _HAS_PTK = True
23
+ except Exception: # pragma: no cover
24
+ _HAS_PTK = False
25
+
26
+
27
+ # --------------------------------------------------------------------------- #
28
+ # Colors
29
+ # --------------------------------------------------------------------------- #
30
+ _TTY = sys.stdout.isatty()
31
+
32
+ def _c(s: str, code: str) -> str:
33
+ return f"\033[{code}m{s}\033[0m" if _TTY else s
34
+
35
+ DIM = "2"; BOLD = "1"; CYAN = "36"; GREEN = "32"; YELLOW = "33"; RED = "31"; BLUE = "34"
36
+
37
+
38
+ INTERACTIVE_SYSTEM = """You are Weio, an expert pair-programmer running as an interactive coding agent in the user's terminal, inside their project directory.
39
+
40
+ You hold a normal conversation AND make code changes. When you change files, emit EDIT BLOCKS:
41
+
42
+ ```edit relative/path/to/file.ext
43
+ <<<<<<< SEARCH
44
+ <exact lines that currently exist>
45
+ =======
46
+ <replacement lines>
47
+ >>>>>>> REPLACE
48
+ ```
49
+
50
+ - SEARCH must match the current file contents EXACTLY. For a NEW file, leave SEARCH empty.
51
+ - You may emit several blocks (and several per file). Keep edits minimal and focused.
52
+ - Before/after edit blocks, talk to the user normally: explain your plan briefly and summarize what you changed.
53
+ - Only edit files relevant to the request. If you need a file you haven't been shown, ask for it (the user can /add it).
54
+ - When the user just asks a question, answer conversationally with no edit blocks."""
55
+
56
+ HELP = """Commands:
57
+ /help show this help
58
+ /add <path> add a file to the working context (re-read each turn)
59
+ /drop <path> remove a file from context
60
+ /files list files in context
61
+ /diff show the last proposed (unapplied) edits
62
+ /auto toggle auto-apply of edits (default: confirm each time)
63
+ /model <id> switch model (e.g. auto, low, mid, pro)
64
+ /undo revert the last applied edit set
65
+ /clear clear the conversation history
66
+ /cwd [path] show or change the working directory
67
+ /exit, /quit leave (Ctrl-D also works)
68
+
69
+ Type anything else to talk to Weio. It can edit files in the project directly."""
70
+
71
+
72
+ class Repl:
73
+ def __init__(self, client: WeioClient, root: Path, max_tokens: int = 4096):
74
+ self.client = client
75
+ self.root = root
76
+ self.max_tokens = max_tokens
77
+ self.messages: list[dict] = [{"role": "system", "content": INTERACTIVE_SYSTEM}]
78
+ self.context_files: list[str] = []
79
+ self.auto_apply = False
80
+ self.pending: list[coder.Edit] = []
81
+ self.undo_stack: list[list[tuple]] = [] # each entry: [(path, old_text_or_None)]
82
+ self._session = None
83
+ # prompt_toolkit needs a real terminal; fall back to plain input otherwise
84
+ # (pipes, CI, dumb terminals).
85
+ if _HAS_PTK and sys.stdin.isatty() and sys.stdout.isatty():
86
+ cfg_dir = Path(os.environ.get("WEIO_CONFIG_DIR", str(Path.home() / ".weio")))
87
+ cfg_dir.mkdir(parents=True, exist_ok=True)
88
+ self._session = PromptSession(
89
+ history=FileHistory(str(cfg_dir / "tui_history")),
90
+ completer=WordCompleter(
91
+ ["/help", "/add", "/drop", "/files", "/diff", "/auto", "/model",
92
+ "/undo", "/clear", "/cwd", "/exit", "/quit"],
93
+ sentence=True,
94
+ ),
95
+ )
96
+
97
+ # ----- input -----
98
+ def _read(self) -> str | None:
99
+ prompt = "weio › "
100
+ try:
101
+ if self._session is not None:
102
+ return self._session.prompt(
103
+ prompt,
104
+ bottom_toolbar=f" {self.root.name} · model={self.client.model} · "
105
+ f"{'AUTO-APPLY' if self.auto_apply else 'confirm edits'} · /help",
106
+ )
107
+ return input(_c(prompt, CYAN))
108
+ except (EOFError, KeyboardInterrupt):
109
+ return None
110
+
111
+ # ----- context -----
112
+ def _context_block(self) -> str:
113
+ blocks = []
114
+ for rel in list(self.context_files):
115
+ fp = self.root / rel
116
+ if not fp.is_file():
117
+ continue
118
+ try:
119
+ blocks.append(f"=== FILE: {rel} ===\n{fp.read_text(encoding='utf-8')}")
120
+ except (OSError, UnicodeDecodeError):
121
+ continue
122
+ return "\n\n".join(blocks)
123
+
124
+ # ----- turn -----
125
+ def _turn(self, text: str):
126
+ user_content = text
127
+ ctx = self._context_block()
128
+ # Auto-include any path-like tokens that exist and aren't already in context
129
+ auto_ctx, _ = coder.gather_context(text, [], self.root)
130
+ extra = auto_ctx if auto_ctx and not ctx else ""
131
+ if ctx or extra:
132
+ user_content += "\n\nFILES IN CONTEXT:\n" + (ctx or extra)
133
+
134
+ self.messages.append({"role": "user", "content": user_content})
135
+
136
+ sys.stdout.write(_c("\nweio: ", GREEN))
137
+ sys.stdout.flush()
138
+ acc = ""
139
+ try:
140
+ for delta in self.client.chat_stream(self.messages, max_tokens=self.max_tokens,
141
+ temperature=0.2):
142
+ acc += delta
143
+ sys.stdout.write(delta)
144
+ sys.stdout.flush()
145
+ print()
146
+ except WeioError as e:
147
+ print("\n" + _c(str(e), RED))
148
+ self.messages.pop()
149
+ return
150
+ self.messages.append({"role": "assistant", "content": acc})
151
+
152
+ edits = coder.parse_edits(acc)
153
+ if edits:
154
+ self._handle_edits(edits)
155
+
156
+ def _handle_edits(self, edits: list[coder.Edit]):
157
+ results, errors = [], []
158
+ for e in edits:
159
+ res, err = coder.apply_edit(e, self.root)
160
+ if err:
161
+ errors.append(err)
162
+ elif res:
163
+ results.append(res)
164
+ for res in results:
165
+ tag = _c("● new ", GREEN) if res.created else _c("● edit ", YELLOW)
166
+ print("\n" + tag + _c(res.path, BOLD))
167
+ if res.created:
168
+ body = res.new_text if len(res.new_text) < 1200 else res.new_text[:1200] + "\n…"
169
+ print(body)
170
+ else:
171
+ print(_color_diff(coder.unified_diff(res.path, res.old_text, res.new_text)))
172
+ for err in errors:
173
+ print(_c(" skip: " + err, RED))
174
+ if not results:
175
+ return
176
+ if not self.auto_apply:
177
+ ans = self._confirm(f"Apply {len(results)} change(s)? [y/N] ")
178
+ if ans not in ("y", "yes"):
179
+ print(_c(" not applied.", DIM))
180
+ return
181
+ undo_entry = []
182
+ for res in results:
183
+ fp = self.root / res.path
184
+ old = fp.read_text(encoding="utf-8") if fp.exists() else None
185
+ undo_entry.append((res.path, old))
186
+ fp.parent.mkdir(parents=True, exist_ok=True)
187
+ fp.write_text(res.new_text, encoding="utf-8")
188
+ self.undo_stack.append(undo_entry)
189
+ for res in results:
190
+ if res.path not in self.context_files:
191
+ self.context_files.append(res.path) # keep edited files in context
192
+ print(_c(f" ✓ applied {len(results)} change(s). (/undo to revert)", GREEN))
193
+
194
+ def _confirm(self, msg: str) -> str:
195
+ try:
196
+ if self._session is not None:
197
+ return self._session.prompt(msg).strip().lower()
198
+ return input(_c(msg, CYAN)).strip().lower()
199
+ except (EOFError, KeyboardInterrupt):
200
+ print()
201
+ return "n"
202
+
203
+ # ----- slash commands -----
204
+ def _slash(self, text: str) -> bool:
205
+ parts = text.strip().split(maxsplit=1)
206
+ cmd = parts[0].lower()
207
+ arg = parts[1].strip() if len(parts) > 1 else ""
208
+ if cmd in ("/exit", "/quit"):
209
+ return False
210
+ elif cmd == "/help":
211
+ print(HELP)
212
+ elif cmd == "/add":
213
+ if not arg:
214
+ print(_c(" usage: /add <path>", DIM))
215
+ elif (self.root / arg).is_file():
216
+ if arg not in self.context_files:
217
+ self.context_files.append(arg)
218
+ print(_c(f" added {arg}", GREEN))
219
+ else:
220
+ print(_c(f" no such file: {arg}", RED))
221
+ elif cmd == "/drop":
222
+ if arg in self.context_files:
223
+ self.context_files.remove(arg)
224
+ print(_c(f" dropped {arg}", GREEN))
225
+ else:
226
+ print(_c(f" not in context: {arg}", DIM))
227
+ elif cmd == "/files":
228
+ if self.context_files:
229
+ for f in self.context_files:
230
+ print(" " + f)
231
+ else:
232
+ print(_c(" (no files in context — /add <path> or just mention them)", DIM))
233
+ elif cmd == "/auto":
234
+ self.auto_apply = not self.auto_apply
235
+ print(_c(f" auto-apply {'ON' if self.auto_apply else 'OFF'}", YELLOW))
236
+ elif cmd == "/model":
237
+ if arg:
238
+ self.client.model = arg
239
+ print(_c(f" model = {arg}", GREEN))
240
+ else:
241
+ print(" model =", self.client.model)
242
+ elif cmd == "/clear":
243
+ self.messages = [{"role": "system", "content": INTERACTIVE_SYSTEM}]
244
+ print(_c(" conversation cleared.", GREEN))
245
+ elif cmd == "/cwd":
246
+ if arg:
247
+ p = (self.root / arg).resolve() if not os.path.isabs(arg) else Path(arg)
248
+ if p.is_dir():
249
+ self.root = p
250
+ print(_c(f" cwd = {p}", GREEN))
251
+ else:
252
+ print(_c(f" not a directory: {arg}", RED))
253
+ else:
254
+ print(" cwd =", self.root)
255
+ elif cmd == "/undo":
256
+ self._undo()
257
+ elif cmd == "/diff":
258
+ print(_c(" (edits are shown when proposed; nothing pending)", DIM))
259
+ else:
260
+ print(_c(f" unknown command: {cmd} (/help)", RED))
261
+ return True
262
+
263
+ def _undo(self):
264
+ if not self.undo_stack:
265
+ print(_c(" nothing to undo.", DIM))
266
+ return
267
+ entry = self.undo_stack.pop()
268
+ for path, old in entry:
269
+ fp = self.root / path
270
+ if old is None:
271
+ try:
272
+ fp.unlink()
273
+ except OSError:
274
+ pass
275
+ else:
276
+ fp.write_text(old, encoding="utf-8")
277
+ print(_c(f" ↩ reverted {len(entry)} file(s).", GREEN))
278
+
279
+ # ----- main loop -----
280
+ def run(self) -> int:
281
+ ok = self.client.ping()
282
+ print(_c(f"weio {__version__}", BOLD) + _c(f" · {self.root}", DIM))
283
+ print(_c(f" {self.client.base} · " +
284
+ ("connected" if ok else "⚠ cannot reach gateway / bad key"),
285
+ GREEN if ok else RED))
286
+ try:
287
+ from .updater import update_notice
288
+ notice = update_notice()
289
+ if notice:
290
+ print(_c(" ⬆ " + notice, YELLOW))
291
+ except Exception:
292
+ pass
293
+ print(_c(" Type a request, or /help. Ctrl-D to exit.\n", DIM))
294
+ while True:
295
+ text = self._read()
296
+ if text is None:
297
+ print()
298
+ break
299
+ text = text.strip()
300
+ if not text:
301
+ continue
302
+ if text.startswith("/"):
303
+ if not self._slash(text):
304
+ break
305
+ continue
306
+ self._turn(text)
307
+ print(_c("bye.", DIM))
308
+ return 0
309
+
310
+
311
+ def _color_diff(diff: str) -> str:
312
+ if not _TTY or not diff:
313
+ return diff or " (no textual diff)"
314
+ out = []
315
+ for line in diff.splitlines():
316
+ if line.startswith("+") and not line.startswith("+++"):
317
+ out.append(_c(line, GREEN))
318
+ elif line.startswith("-") and not line.startswith("---"):
319
+ out.append(_c(line, RED))
320
+ elif line.startswith("@@"):
321
+ out.append(_c(line, CYAN))
322
+ else:
323
+ out.append(line)
324
+ return "\n".join(out)
325
+
326
+
327
+ def run_tui(client: WeioClient, root: Path, max_tokens: int = 4096) -> int:
328
+ return Repl(client, root, max_tokens=max_tokens).run()
@@ -0,0 +1,114 @@
1
+ """Update checking + self-update.
2
+
3
+ A throttled, fail-silent check against PyPI (once per day) tells the user when a
4
+ newer weio-cli is published. `weio update` upgrades in place. Disable the check
5
+ with WEIO_NO_UPDATE_CHECK=1.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import subprocess
12
+ import sys
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ from . import __version__
18
+
19
+ INDEX_JSON = os.environ.get("WEIO_UPDATE_INDEX", "https://pypi.org/pypi/weio-cli/json")
20
+ _CACHE_NAME = "update_check.json"
21
+ _TTL_HOURS = 24
22
+
23
+
24
+ def _cache_dir() -> Path:
25
+ return Path(os.environ.get("WEIO_CONFIG_DIR", str(Path.home() / ".weio")))
26
+
27
+
28
+ def _parse_version(v: str) -> tuple:
29
+ """Lenient numeric version tuple, e.g. '0.10.2' -> (0, 10, 2)."""
30
+ out = []
31
+ for part in (v or "").split("."):
32
+ num = ""
33
+ for ch in part:
34
+ if ch.isdigit():
35
+ num += ch
36
+ else:
37
+ break
38
+ out.append(int(num) if num else 0)
39
+ return tuple(out) or (0,)
40
+
41
+
42
+ def is_newer(latest: str, current: str) -> bool:
43
+ return _parse_version(latest) > _parse_version(current)
44
+
45
+
46
+ def fetch_latest(timeout: float = 2.5) -> Optional[str]:
47
+ try:
48
+ import httpx
49
+ r = httpx.get(INDEX_JSON, timeout=timeout,
50
+ headers={"User-Agent": f"weio-cli/{__version__}"})
51
+ if r.status_code != 200:
52
+ return None
53
+ return r.json()["info"]["version"]
54
+ except Exception:
55
+ return None
56
+
57
+
58
+ def check_for_update(current: str = __version__, *, ttl_hours: int = _TTL_HOURS,
59
+ force: bool = False) -> Optional[str]:
60
+ """Return the latest version string if newer than `current`, else None.
61
+
62
+ Throttled to once per ttl_hours via a cache file; never raises.
63
+ """
64
+ if os.environ.get("WEIO_NO_UPDATE_CHECK") == "1":
65
+ return None
66
+ try:
67
+ cache = _cache_dir() / _CACHE_NAME
68
+ data = {}
69
+ if cache.exists():
70
+ try:
71
+ data = json.loads(cache.read_text())
72
+ except Exception:
73
+ data = {}
74
+ now = time.time()
75
+ latest = data.get("latest")
76
+ last = float(data.get("last_check", 0) or 0)
77
+
78
+ if force or not latest or (now - last) > ttl_hours * 3600:
79
+ fetched = fetch_latest()
80
+ if fetched:
81
+ latest = fetched
82
+ try:
83
+ cache.parent.mkdir(parents=True, exist_ok=True)
84
+ cache.write_text(json.dumps({"last_check": now, "latest": latest}))
85
+ except Exception:
86
+ pass
87
+
88
+ if latest and is_newer(latest, current):
89
+ return latest
90
+ except Exception:
91
+ pass
92
+ return None
93
+
94
+
95
+ def update_notice(current: str = __version__) -> Optional[str]:
96
+ """One-line, color-free notice if an update is available (for printing)."""
97
+ latest = check_for_update(current)
98
+ if not latest:
99
+ return None
100
+ return (f"weio-cli {latest} is available (you have {current}). "
101
+ f"Update with: weio update")
102
+
103
+
104
+ def self_update(pre: bool = False) -> int:
105
+ """Upgrade weio-cli in place using the running interpreter's pip."""
106
+ cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "weio-cli"]
107
+ if pre:
108
+ cmd.append("--pre")
109
+ print(f"Running: {' '.join(cmd)}")
110
+ try:
111
+ return subprocess.call(cmd)
112
+ except Exception as e:
113
+ print(f"Update failed: {e}", file=sys.stderr)
114
+ return 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weio-cli
3
- Version: 0.1.0
3
+ Version: 0.3.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
@@ -17,6 +17,7 @@ Requires-Python: >=3.9
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: httpx>=0.24
20
+ Requires-Dist: prompt_toolkit>=3.0
20
21
  Dynamic: license-file
21
22
 
22
23
  # weio-cli
@@ -44,8 +45,36 @@ export WEIO_API_KEY="weio_sk_…"
44
45
 
45
46
  ## Use
46
47
 
48
+ ### Interactive TUI (default)
49
+
50
+ Run `weio` with no arguments to drop into an interactive coding session — like a
51
+ local pair-programmer in your terminal:
52
+
53
+ ```bash
54
+ cd my-project
55
+ weio
56
+ ```
57
+
58
+ ```
59
+ weio › add a /health route to app.py that returns {"ok": true}
60
+
61
+ weio: I'll add a health check route…
62
+ ● edit app.py
63
+ --- a/app.py
64
+ +++ b/app.py
65
+ @@ …
66
+ Apply 1 change(s)? [y/N] y
67
+ ✓ applied 1 change(s). (/undo to revert)
68
+ ```
69
+
70
+ Slash commands: `/add <file>`, `/drop <file>`, `/files`, `/auto` (auto-apply),
71
+ `/model <id>`, `/undo`, `/clear`, `/cwd`, `/help`, `/exit`. Arrow-up recalls
72
+ history. Files you mention or `/add` are kept in context and re-read each turn.
73
+
74
+ ### One-shot & other commands
75
+
47
76
  ```bash
48
- # Run a coding task in the current directory (reads & edits files):
77
+ # Run a single coding task non-interactively (reads & edits files):
49
78
  weio "add error handling to the fetch() in api.py"
50
79
 
51
80
  # Add specific files to the context:
@@ -64,6 +93,19 @@ weio ping
64
93
  Edits are shown as a diff and require confirmation before anything is written
65
94
  (use `-y`/`--yes` to apply automatically). New files are created as needed.
66
95
 
96
+ ## Updating
97
+
98
+ weio-cli checks PyPI once a day (fail-silent) and prints a one-line notice when a
99
+ newer version is available. To upgrade:
100
+
101
+ ```bash
102
+ weio update # upgrades in place via pip
103
+ # or
104
+ pip install -U weio-cli
105
+ ```
106
+
107
+ Disable the check with `WEIO_NO_UPDATE_CHECK=1`.
108
+
67
109
  ## Configuration
68
110
 
69
111
  | Setting | Flag | Env | Config file |
@@ -7,6 +7,8 @@ src/weio_cli/cli.py
7
7
  src/weio_cli/client.py
8
8
  src/weio_cli/coder.py
9
9
  src/weio_cli/config.py
10
+ src/weio_cli/tui.py
11
+ src/weio_cli/updater.py
10
12
  src/weio_cli.egg-info/PKG-INFO
11
13
  src/weio_cli.egg-info/SOURCES.txt
12
14
  src/weio_cli.egg-info/dependency_links.txt
@@ -0,0 +1,2 @@
1
+ httpx>=0.24
2
+ prompt_toolkit>=3.0
@@ -1 +0,0 @@
1
- httpx>=0.24
File without changes
File without changes
File without changes
File without changes