weio-cli 0.3.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.
- weio_cli-0.5.0/PKG-INFO +159 -0
- weio_cli-0.5.0/README.md +137 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/pyproject.toml +1 -1
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli/__init__.py +1 -1
- weio_cli-0.5.0/src/weio_cli/agent.py +237 -0
- weio_cli-0.5.0/src/weio_cli/browser_login.py +88 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli/cli.py +90 -67
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli/client.py +15 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli/config.py +1 -1
- weio_cli-0.5.0/src/weio_cli/settings.py +69 -0
- weio_cli-0.5.0/src/weio_cli/tools.py +167 -0
- weio_cli-0.5.0/src/weio_cli/tui.py +345 -0
- weio_cli-0.5.0/src/weio_cli.egg-info/PKG-INFO +159 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli.egg-info/SOURCES.txt +5 -2
- weio_cli-0.5.0/tests/test_agent.py +90 -0
- weio_cli-0.3.0/PKG-INFO +0 -119
- weio_cli-0.3.0/README.md +0 -97
- weio_cli-0.3.0/src/weio_cli/coder.py +0 -160
- weio_cli-0.3.0/src/weio_cli/tui.py +0 -328
- weio_cli-0.3.0/src/weio_cli.egg-info/PKG-INFO +0 -119
- weio_cli-0.3.0/tests/test_coder.py +0 -87
- {weio_cli-0.3.0 → weio_cli-0.5.0}/LICENSE +0 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/setup.cfg +0 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli/__main__.py +0 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli/updater.py +0 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli.egg-info/dependency_links.txt +0 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli.egg-info/entry_points.txt +0 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli.egg-info/requires.txt +0 -0
- {weio_cli-0.3.0 → weio_cli-0.5.0}/src/weio_cli.egg-info/top_level.txt +0 -0
weio_cli-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: weio-cli
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Weio — an agentic coding assistant that routes inference through your Weio account.
|
|
5
|
+
Author: We I/O Labs
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://weio.ai
|
|
8
|
+
Project-URL: Documentation, https://weio.ai/support
|
|
9
|
+
Keywords: weio,ai,cli,coding-assistant,llm
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: httpx>=0.24
|
|
20
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# weio-cli
|
|
24
|
+
|
|
25
|
+
An agentic coding assistant that runs on your machine and routes inference
|
|
26
|
+
through your [Weio](https://weio.ai) account.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install weio-cli # or: pipx install weio-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.9+.
|
|
35
|
+
|
|
36
|
+
## Authenticate
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
weio login # opens your browser, sign in (incl. Google), key is created automatically
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`weio login` starts a one-time local handshake, opens weio.ai in your browser to
|
|
43
|
+
sign in, mints an API key for this device, and saves it to `~/.weio/config.json`.
|
|
44
|
+
No copy-paste.
|
|
45
|
+
|
|
46
|
+
Prefer to paste a key yourself? Generate one in **Settings → API & CLI** on
|
|
47
|
+
weio.ai and:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
weio login --no-browser # paste your weio_sk_… key
|
|
51
|
+
# or, per session:
|
|
52
|
+
export WEIO_API_KEY="weio_sk_…"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Check what you're using:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
weio usage # tier, tokens used today, remaining, reset time
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Use
|
|
62
|
+
|
|
63
|
+
### Interactive agent (default)
|
|
64
|
+
|
|
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:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cd my-project
|
|
71
|
+
weio
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```
|
|
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.
|
|
88
|
+
```
|
|
89
|
+
|
|
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.
|
|
117
|
+
|
|
118
|
+
### One-shot & other commands
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Run a coding task non-interactively (auto-applies edits; add --auto to also run commands):
|
|
122
|
+
weio "add error handling to the fetch() in api.py"
|
|
123
|
+
weio code "refactor db.py to async" --auto
|
|
124
|
+
|
|
125
|
+
# Quick question (no file edits):
|
|
126
|
+
weio ask "what does a 502 from nginx usually mean?"
|
|
127
|
+
|
|
128
|
+
weio chat # plain interactive chat
|
|
129
|
+
weio usage # tokens used / limits
|
|
130
|
+
weio ping # connectivity + key check
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Edits are shown as a diff and require confirmation before anything is written
|
|
134
|
+
(use `-y`/`--yes` to apply automatically). New files are created as needed.
|
|
135
|
+
|
|
136
|
+
## Updating
|
|
137
|
+
|
|
138
|
+
weio-cli checks PyPI once a day (fail-silent) and prints a one-line notice when a
|
|
139
|
+
newer version is available. To upgrade:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
weio update # upgrades in place via pip
|
|
143
|
+
# or
|
|
144
|
+
pip install -U weio-cli
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Disable the check with `WEIO_NO_UPDATE_CHECK=1`.
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
| Setting | Flag | Env | Config file |
|
|
152
|
+
|---|---|---|---|
|
|
153
|
+
| API key | `--key` | `WEIO_API_KEY` | `~/.weio/config.json` |
|
|
154
|
+
| API base | `--base` | `WEIO_BASE` | `~/.weio/config.json` |
|
|
155
|
+
| Model | `--model` | — | — |
|
|
156
|
+
|
|
157
|
+
Self-hosted / LAN gateway? Point at it with `--base http://HOST:8901/v1`.
|
|
158
|
+
|
|
159
|
+
Output is billed against your Weio account usage.
|
weio_cli-0.5.0/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# weio-cli
|
|
2
|
+
|
|
3
|
+
An agentic coding assistant that runs on your machine and routes inference
|
|
4
|
+
through your [Weio](https://weio.ai) account.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install weio-cli # or: pipx install weio-cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires Python 3.9+.
|
|
13
|
+
|
|
14
|
+
## Authenticate
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
weio login # opens your browser, sign in (incl. Google), key is created automatically
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`weio login` starts a one-time local handshake, opens weio.ai in your browser to
|
|
21
|
+
sign in, mints an API key for this device, and saves it to `~/.weio/config.json`.
|
|
22
|
+
No copy-paste.
|
|
23
|
+
|
|
24
|
+
Prefer to paste a key yourself? Generate one in **Settings → API & CLI** on
|
|
25
|
+
weio.ai and:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
weio login --no-browser # paste your weio_sk_… key
|
|
29
|
+
# or, per session:
|
|
30
|
+
export WEIO_API_KEY="weio_sk_…"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Check what you're using:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
weio usage # tier, tokens used today, remaining, reset time
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Use
|
|
40
|
+
|
|
41
|
+
### Interactive agent (default)
|
|
42
|
+
|
|
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:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd my-project
|
|
49
|
+
weio
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```
|
|
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.
|
|
66
|
+
```
|
|
67
|
+
|
|
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.
|
|
95
|
+
|
|
96
|
+
### One-shot & other commands
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Run a coding task non-interactively (auto-applies edits; add --auto to also run commands):
|
|
100
|
+
weio "add error handling to the fetch() in api.py"
|
|
101
|
+
weio code "refactor db.py to async" --auto
|
|
102
|
+
|
|
103
|
+
# Quick question (no file edits):
|
|
104
|
+
weio ask "what does a 502 from nginx usually mean?"
|
|
105
|
+
|
|
106
|
+
weio chat # plain interactive chat
|
|
107
|
+
weio usage # tokens used / limits
|
|
108
|
+
weio ping # connectivity + key check
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Edits are shown as a diff and require confirmation before anything is written
|
|
112
|
+
(use `-y`/`--yes` to apply automatically). New files are created as needed.
|
|
113
|
+
|
|
114
|
+
## Updating
|
|
115
|
+
|
|
116
|
+
weio-cli checks PyPI once a day (fail-silent) and prints a one-line notice when a
|
|
117
|
+
newer version is available. To upgrade:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
weio update # upgrades in place via pip
|
|
121
|
+
# or
|
|
122
|
+
pip install -U weio-cli
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Disable the check with `WEIO_NO_UPDATE_CHECK=1`.
|
|
126
|
+
|
|
127
|
+
## Configuration
|
|
128
|
+
|
|
129
|
+
| Setting | Flag | Env | Config file |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| API key | `--key` | `WEIO_API_KEY` | `~/.weio/config.json` |
|
|
132
|
+
| API base | `--base` | `WEIO_BASE` | `~/.weio/config.json` |
|
|
133
|
+
| Model | `--model` | — | — |
|
|
134
|
+
|
|
135
|
+
Self-hosted / LAN gateway? Point at it with `--base http://HOST:8901/v1`.
|
|
136
|
+
|
|
137
|
+
Output is billed against your Weio account usage.
|
|
@@ -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.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"
|
|
@@ -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"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Browser-based login: open Weio in the browser, sign in (incl. Google), and
|
|
2
|
+
receive a freshly-minted API key on a local loopback server. No manual key paste.
|
|
3
|
+
|
|
4
|
+
Standard OAuth-CLI loopback pattern (like gcloud / gh): we start a localhost
|
|
5
|
+
server, open the browser to the Weio authorize page with our port + a random
|
|
6
|
+
state, the page mints a key and redirects back to the loopback with it.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import http.server
|
|
11
|
+
import secrets
|
|
12
|
+
import socketserver
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
import urllib.parse
|
|
16
|
+
import webbrowser
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _web_base_from_api(api_base: str) -> str:
|
|
21
|
+
"""https://weio.ai/v1 -> https://weio.ai"""
|
|
22
|
+
b = (api_base or "").rstrip("/")
|
|
23
|
+
if b.endswith("/v1"):
|
|
24
|
+
b = b[:-3]
|
|
25
|
+
return b.rstrip("/")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_DONE_HTML = (
|
|
29
|
+
b"<!doctype html><html><head><meta charset='utf-8'><title>Weio CLI</title></head>"
|
|
30
|
+
b"<body style='font-family:-apple-system,Segoe UI,sans-serif;background:#0d1117;"
|
|
31
|
+
b"color:#e6edf3;display:flex;align-items:center;justify-content:center;height:100vh;margin:0'>"
|
|
32
|
+
b"<div style='text-align:center'><h2 style='color:#3fb950'>✓ Weio CLI authorized</h2>"
|
|
33
|
+
b"<p>You can close this tab and return to your terminal.</p></div></body></html>"
|
|
34
|
+
)
|
|
35
|
+
_FAIL_HTML = (
|
|
36
|
+
b"<!doctype html><html><body style='font-family:sans-serif'>"
|
|
37
|
+
b"<h2>Authorization failed.</h2><p>Please run <code>weio login</code> again.</p></body></html>"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def browser_login(api_base: str, timeout: float = 180.0) -> Optional[str]:
|
|
42
|
+
"""Open the browser and return the minted API key, or None on failure/timeout."""
|
|
43
|
+
state = secrets.token_urlsafe(16)
|
|
44
|
+
result: dict = {}
|
|
45
|
+
|
|
46
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
47
|
+
def do_GET(self): # noqa: N802
|
|
48
|
+
params = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
|
49
|
+
key = (params.get("key") or [None])[0]
|
|
50
|
+
st = (params.get("state") or [None])[0]
|
|
51
|
+
ok = bool(key) and st == state
|
|
52
|
+
self.send_response(200)
|
|
53
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
54
|
+
self.end_headers()
|
|
55
|
+
self.wfile.write(_DONE_HTML if ok else _FAIL_HTML)
|
|
56
|
+
if ok:
|
|
57
|
+
result["key"] = key
|
|
58
|
+
|
|
59
|
+
def log_message(self, *args): # silence
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
httpd = socketserver.TCPServer(("127.0.0.1", 0), Handler)
|
|
64
|
+
except OSError as e:
|
|
65
|
+
print(f"Could not start local auth server: {e}", file=sys.stderr)
|
|
66
|
+
return None
|
|
67
|
+
httpd.timeout = 1
|
|
68
|
+
port = httpd.server_address[1]
|
|
69
|
+
auth_url = (f"{_web_base_from_api(api_base)}/cli/auth"
|
|
70
|
+
f"?port={port}&state={urllib.parse.quote(state)}")
|
|
71
|
+
|
|
72
|
+
print("Opening your browser to sign in to Weio…")
|
|
73
|
+
print(f" {auth_url}")
|
|
74
|
+
print(" (If it doesn't open, copy that URL into your browser.)")
|
|
75
|
+
try:
|
|
76
|
+
webbrowser.open(auth_url)
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
deadline = time.time() + timeout
|
|
81
|
+
try:
|
|
82
|
+
while "key" not in result and time.time() < deadline:
|
|
83
|
+
httpd.handle_request()
|
|
84
|
+
except KeyboardInterrupt:
|
|
85
|
+
pass
|
|
86
|
+
finally:
|
|
87
|
+
httpd.server_close()
|
|
88
|
+
return result.get("key")
|