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.
- {weio_cli-0.1.0/src/weio_cli.egg-info → weio_cli-0.3.0}/PKG-INFO +44 -2
- {weio_cli-0.1.0 → weio_cli-0.3.0}/README.md +42 -1
- {weio_cli-0.1.0 → weio_cli-0.3.0}/pyproject.toml +2 -2
- {weio_cli-0.1.0 → weio_cli-0.3.0}/src/weio_cli/__init__.py +1 -1
- {weio_cli-0.1.0 → weio_cli-0.3.0}/src/weio_cli/cli.py +49 -4
- {weio_cli-0.1.0 → weio_cli-0.3.0}/src/weio_cli/client.py +20 -0
- weio_cli-0.3.0/src/weio_cli/tui.py +328 -0
- weio_cli-0.3.0/src/weio_cli/updater.py +114 -0
- {weio_cli-0.1.0 → weio_cli-0.3.0/src/weio_cli.egg-info}/PKG-INFO +44 -2
- {weio_cli-0.1.0 → weio_cli-0.3.0}/src/weio_cli.egg-info/SOURCES.txt +2 -0
- weio_cli-0.3.0/src/weio_cli.egg-info/requires.txt +2 -0
- weio_cli-0.1.0/src/weio_cli.egg-info/requires.txt +0 -1
- {weio_cli-0.1.0 → weio_cli-0.3.0}/LICENSE +0 -0
- {weio_cli-0.1.0 → weio_cli-0.3.0}/setup.cfg +0 -0
- {weio_cli-0.1.0 → weio_cli-0.3.0}/src/weio_cli/__main__.py +0 -0
- {weio_cli-0.1.0 → weio_cli-0.3.0}/src/weio_cli/coder.py +0 -0
- {weio_cli-0.1.0 → weio_cli-0.3.0}/src/weio_cli/config.py +0 -0
- {weio_cli-0.1.0 → weio_cli-0.3.0}/src/weio_cli.egg-info/dependency_links.txt +0 -0
- {weio_cli-0.1.0 → weio_cli-0.3.0}/src/weio_cli.egg-info/entry_points.txt +0 -0
- {weio_cli-0.1.0 → weio_cli-0.3.0}/src/weio_cli.egg-info/top_level.txt +0 -0
- {weio_cli-0.1.0 → weio_cli-0.3.0}/tests/test_coder.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weio-cli
|
|
3
|
-
Version: 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
|
|
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
|
|
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.
|
|
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"
|
|
@@ -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
|
-
|
|
257
|
-
|
|
258
|
-
if
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 |
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
httpx>=0.24
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|