weio-cli 0.1.0__tar.gz → 0.2.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.2.0}/PKG-INFO +31 -2
- {weio_cli-0.1.0 → weio_cli-0.2.0}/README.md +29 -1
- {weio_cli-0.1.0 → weio_cli-0.2.0}/pyproject.toml +2 -2
- {weio_cli-0.1.0 → weio_cli-0.2.0}/src/weio_cli/__init__.py +1 -1
- {weio_cli-0.1.0 → weio_cli-0.2.0}/src/weio_cli/cli.py +24 -3
- {weio_cli-0.1.0 → weio_cli-0.2.0}/src/weio_cli/client.py +20 -0
- weio_cli-0.2.0/src/weio_cli/tui.py +321 -0
- {weio_cli-0.1.0 → weio_cli-0.2.0/src/weio_cli.egg-info}/PKG-INFO +31 -2
- {weio_cli-0.1.0 → weio_cli-0.2.0}/src/weio_cli.egg-info/SOURCES.txt +1 -0
- weio_cli-0.2.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.2.0}/LICENSE +0 -0
- {weio_cli-0.1.0 → weio_cli-0.2.0}/setup.cfg +0 -0
- {weio_cli-0.1.0 → weio_cli-0.2.0}/src/weio_cli/__main__.py +0 -0
- {weio_cli-0.1.0 → weio_cli-0.2.0}/src/weio_cli/coder.py +0 -0
- {weio_cli-0.1.0 → weio_cli-0.2.0}/src/weio_cli/config.py +0 -0
- {weio_cli-0.1.0 → weio_cli-0.2.0}/src/weio_cli.egg-info/dependency_links.txt +0 -0
- {weio_cli-0.1.0 → weio_cli-0.2.0}/src/weio_cli.egg-info/entry_points.txt +0 -0
- {weio_cli-0.1.0 → weio_cli-0.2.0}/src/weio_cli.egg-info/top_level.txt +0 -0
- {weio_cli-0.1.0 → weio_cli-0.2.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.2.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:
|
|
@@ -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:
|
|
@@ -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.2.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,20 @@ def cmd_chat(args) -> int:
|
|
|
128
128
|
return 0
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def cmd_tui(args) -> int:
|
|
132
|
+
try:
|
|
133
|
+
client = _make_client(args)
|
|
134
|
+
except WeioError as e:
|
|
135
|
+
print(_c(str(e), "31"), file=sys.stderr)
|
|
136
|
+
return 2
|
|
137
|
+
root = Path(getattr(args, "dir", ".") or ".").resolve()
|
|
138
|
+
if not root.is_dir():
|
|
139
|
+
print(_c(f"Not a directory: {root}", "31"), file=sys.stderr)
|
|
140
|
+
return 2
|
|
141
|
+
from .tui import run_tui
|
|
142
|
+
return run_tui(client, root, max_tokens=_max_tokens(args))
|
|
143
|
+
|
|
144
|
+
|
|
131
145
|
def cmd_code(args) -> int:
|
|
132
146
|
try:
|
|
133
147
|
client = _make_client(args)
|
|
@@ -239,6 +253,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
239
253
|
sp = sub.add_parser("chat", parents=[common], help="Interactive chat")
|
|
240
254
|
sp.set_defaults(func=cmd_chat)
|
|
241
255
|
|
|
256
|
+
sp = sub.add_parser("tui", parents=[common], help="Interactive coding TUI (default when run with no args)")
|
|
257
|
+
sp.add_argument("--dir", "-C", default=".", help="Project directory (default: cwd)")
|
|
258
|
+
sp.set_defaults(func=cmd_tui)
|
|
259
|
+
|
|
242
260
|
sp = sub.add_parser("code", parents=[common], help="Run a coding task in a directory")
|
|
243
261
|
sp.add_argument("message")
|
|
244
262
|
sp.add_argument("--file", "-f", action="append", help="Add a file to the context (repeatable)")
|
|
@@ -253,9 +271,12 @@ def main(argv=None) -> int:
|
|
|
253
271
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
254
272
|
parser = build_parser()
|
|
255
273
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if
|
|
274
|
+
known = {"login", "ping", "ask", "chat", "code", "tui"}
|
|
275
|
+
# Bare `weio` (no args) → interactive coding TUI, like Claude Code.
|
|
276
|
+
if not argv:
|
|
277
|
+
argv = ["tui"]
|
|
278
|
+
# Bare `weio "do something"` (a message, not a flag/command) → one-shot code.
|
|
279
|
+
elif argv[0] not in known and not argv[0].startswith("-"):
|
|
259
280
|
argv = ["code", *argv]
|
|
260
281
|
|
|
261
282
|
args = parser.parse_args(argv)
|
|
@@ -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,321 @@
|
|
|
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
|
+
print(_c(" Type a request, or /help. Ctrl-D to exit.\n", DIM))
|
|
287
|
+
while True:
|
|
288
|
+
text = self._read()
|
|
289
|
+
if text is None:
|
|
290
|
+
print()
|
|
291
|
+
break
|
|
292
|
+
text = text.strip()
|
|
293
|
+
if not text:
|
|
294
|
+
continue
|
|
295
|
+
if text.startswith("/"):
|
|
296
|
+
if not self._slash(text):
|
|
297
|
+
break
|
|
298
|
+
continue
|
|
299
|
+
self._turn(text)
|
|
300
|
+
print(_c("bye.", DIM))
|
|
301
|
+
return 0
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _color_diff(diff: str) -> str:
|
|
305
|
+
if not _TTY or not diff:
|
|
306
|
+
return diff or " (no textual diff)"
|
|
307
|
+
out = []
|
|
308
|
+
for line in diff.splitlines():
|
|
309
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
310
|
+
out.append(_c(line, GREEN))
|
|
311
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
312
|
+
out.append(_c(line, RED))
|
|
313
|
+
elif line.startswith("@@"):
|
|
314
|
+
out.append(_c(line, CYAN))
|
|
315
|
+
else:
|
|
316
|
+
out.append(line)
|
|
317
|
+
return "\n".join(out)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def run_tui(client: WeioClient, root: Path, max_tokens: int = 4096) -> int:
|
|
321
|
+
return Repl(client, root, max_tokens=max_tokens).run()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weio-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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:
|
|
@@ -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
|