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