webbee 0.1.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.
- webbee-0.1.0/.github/workflows/publish.yml +28 -0
- webbee-0.1.0/.gitignore +9 -0
- webbee-0.1.0/CHANGELOG.md +18 -0
- webbee-0.1.0/LICENSE +21 -0
- webbee-0.1.0/PKG-INFO +86 -0
- webbee-0.1.0/README.md +54 -0
- webbee-0.1.0/install.sh +17 -0
- webbee-0.1.0/pyproject.toml +44 -0
- webbee-0.1.0/src/webbee/__init__.py +1 -0
- webbee-0.1.0/src/webbee/account.py +78 -0
- webbee-0.1.0/src/webbee/banner_art.py +9 -0
- webbee-0.1.0/src/webbee/cli.py +56 -0
- webbee-0.1.0/src/webbee/commands.py +77 -0
- webbee-0.1.0/src/webbee/config.py +14 -0
- webbee-0.1.0/src/webbee/events.py +17 -0
- webbee-0.1.0/src/webbee/render.py +313 -0
- webbee-0.1.0/src/webbee/repl.py +157 -0
- webbee-0.1.0/src/webbee/session.py +160 -0
- webbee-0.1.0/src/webbee/tools.py +84 -0
- webbee-0.1.0/src/webbee/tui.py +217 -0
- webbee-0.1.0/src/webbee/update.py +52 -0
- webbee-0.1.0/tests/__init__.py +0 -0
- webbee-0.1.0/tests/test_account.py +50 -0
- webbee-0.1.0/tests/test_cli.py +31 -0
- webbee-0.1.0/tests/test_commands.py +109 -0
- webbee-0.1.0/tests/test_config.py +14 -0
- webbee-0.1.0/tests/test_events.py +36 -0
- webbee-0.1.0/tests/test_packaging.py +28 -0
- webbee-0.1.0/tests/test_render.py +243 -0
- webbee-0.1.0/tests/test_repl.py +165 -0
- webbee-0.1.0/tests/test_session.py +85 -0
- webbee-0.1.0/tests/test_tools.py +38 -0
- webbee-0.1.0/tests/test_tui.py +72 -0
- webbee-0.1.0/tests/test_update.py +49 -0
- webbee-0.1.0/tests/test_version.py +14 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publish to PyPI on a version tag (e.g. `v0.1.0`), matching the Imperal
|
|
4
|
+
# convention (imperal-mcp). Auth uses the account API token stored as the
|
|
5
|
+
# GitHub Actions secret `PYPI_API_TOKEN` — add it to this repo, or set an
|
|
6
|
+
# `imperalcloud` org-level secret so every package inherits it. The token is
|
|
7
|
+
# NEVER stored in the repo/files; only in the encrypted Actions secret.
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
tags: ["v*"]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
- name: Build sdist + wheel
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade build
|
|
24
|
+
python -m build
|
|
25
|
+
- name: Publish to PyPI
|
|
26
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
27
|
+
with:
|
|
28
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
webbee-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First public release.
|
|
6
|
+
|
|
7
|
+
- `webbee` — a coding agent in your terminal: reads, writes, and runs code in
|
|
8
|
+
the current directory; the brain runs in the Imperal Cloud on ICNLI. No model
|
|
9
|
+
keys on the machine.
|
|
10
|
+
- Full-screen dock: a scrollable, colored output pane with the input box and
|
|
11
|
+
toolbar pinned at the bottom.
|
|
12
|
+
- Consent modes — **default** (asks before anything it can't undo), **plan**
|
|
13
|
+
(read-only), **autopilot** (acts without asking); cycle with **Shift + TAB**.
|
|
14
|
+
Spending money always needs a browser approval.
|
|
15
|
+
- Reaches your connected Imperal apps (mail, notes, tasks, …) alongside the
|
|
16
|
+
local code tools.
|
|
17
|
+
- Slash commands: `/login` `/logout` `/mode` `/cost` `/status` `/clear` `/exit`.
|
|
18
|
+
- Live token/cost meter (session total) and update check.
|
webbee-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Imperal, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
webbee-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webbee
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Webbee 🐝 — the Imperal Cloud coding agent in your terminal
|
|
5
|
+
Project-URL: Homepage, https://imperal.io
|
|
6
|
+
Project-URL: Documentation, https://docs.imperal.io
|
|
7
|
+
Project-URL: Repository, https://github.com/imperalcloud/webbee-code
|
|
8
|
+
Project-URL: Issues, https://github.com/imperalcloud/webbee-code/issues
|
|
9
|
+
Project-URL: Protocol, https://icnli.org
|
|
10
|
+
Author-email: "Imperal, Inc." <hello@imperal.io>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai-agent,cli,coding-agent,icnli,imperal,llm,terminal,webbee
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: httpx-sse>=0.4
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: imperal-mcp>=0.4.0
|
|
28
|
+
Requires-Dist: imperal-sdk>=5.9.2
|
|
29
|
+
Requires-Dist: prompt-toolkit>=3
|
|
30
|
+
Requires-Dist: rich>=13.7
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Webbee 🐝 — the coding agent in your terminal
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/webbee/)
|
|
36
|
+
[](https://pypi.org/project/webbee/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
[](https://docs.imperal.io)
|
|
39
|
+
|
|
40
|
+
Webbee is the [Imperal Cloud](https://imperal.io) coding agent, in your terminal. It reads, writes, and runs code in your working directory — while the brain runs in the cloud on **ICNLI**, the open protocol behind Webbee. No model keys on your machine. Swap the model underneath and it behaves the same, because the safety was never in the model.
|
|
41
|
+
|
|
42
|
+
**The model proposes. The kernel decides. The key — delete, drop, wipe — stays with you.**
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pipx install webbee # or: uv tool install webbee
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
No Python on the box? One line:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
curl -LsSf https://webbee.imperal.io/install.sh | sh
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Use
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
webbee # start the agent in the current directory
|
|
60
|
+
webbee login # sign in to your Imperal account (opens the browser)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Type in plain English. Webbee reads your files, runs commands, and reaches your connected Imperal apps — mail, notes, tasks, and more — to get the job done. `/help` lists the commands: `/login` `/logout` `/mode` `/cost` `/status` `/clear` `/exit`.
|
|
64
|
+
|
|
65
|
+
## Modes — you hold the key
|
|
66
|
+
|
|
67
|
+
Cycle with **Shift + TAB**:
|
|
68
|
+
|
|
69
|
+
- **default** — Webbee does the small, reversible stuff herself. Anything she can't undo, she stops and asks you first.
|
|
70
|
+
- **plan** — read-only. She plans and reads; she touches nothing.
|
|
71
|
+
- **autopilot** — she acts without asking. (Spending money always needs a browser approval — no terminal reply can release it.)
|
|
72
|
+
|
|
73
|
+
## How it works
|
|
74
|
+
|
|
75
|
+
Your machine runs the hands — read, write, edit, run. The brain runs in the Imperal Cloud and reasons over your files, your history, and your connected apps through ICNLI. The model is a replaceable proposer at the edge; the kernel resolves, grounds, and decides. Webbee reads your facts. She doesn't invent them.
|
|
76
|
+
|
|
77
|
+
## Links
|
|
78
|
+
|
|
79
|
+
- **Imperal Cloud** — [imperal.io](https://imperal.io)
|
|
80
|
+
- **Docs** — [docs.imperal.io](https://docs.imperal.io)
|
|
81
|
+
- **ICNLI** — the open protocol, CC BY-SA 4.0 — [icnli.org](https://icnli.org)
|
|
82
|
+
- **More from Imperal** — [github.com/imperalcloud](https://github.com/imperalcloud)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
MIT © Imperal, Inc.
|
webbee-0.1.0/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Webbee 🐝 — the coding agent in your terminal
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/webbee/)
|
|
4
|
+
[](https://pypi.org/project/webbee/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://docs.imperal.io)
|
|
7
|
+
|
|
8
|
+
Webbee is the [Imperal Cloud](https://imperal.io) coding agent, in your terminal. It reads, writes, and runs code in your working directory — while the brain runs in the cloud on **ICNLI**, the open protocol behind Webbee. No model keys on your machine. Swap the model underneath and it behaves the same, because the safety was never in the model.
|
|
9
|
+
|
|
10
|
+
**The model proposes. The kernel decides. The key — delete, drop, wipe — stays with you.**
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pipx install webbee # or: uv tool install webbee
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
No Python on the box? One line:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
curl -LsSf https://webbee.imperal.io/install.sh | sh
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Use
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
webbee # start the agent in the current directory
|
|
28
|
+
webbee login # sign in to your Imperal account (opens the browser)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Type in plain English. Webbee reads your files, runs commands, and reaches your connected Imperal apps — mail, notes, tasks, and more — to get the job done. `/help` lists the commands: `/login` `/logout` `/mode` `/cost` `/status` `/clear` `/exit`.
|
|
32
|
+
|
|
33
|
+
## Modes — you hold the key
|
|
34
|
+
|
|
35
|
+
Cycle with **Shift + TAB**:
|
|
36
|
+
|
|
37
|
+
- **default** — Webbee does the small, reversible stuff herself. Anything she can't undo, she stops and asks you first.
|
|
38
|
+
- **plan** — read-only. She plans and reads; she touches nothing.
|
|
39
|
+
- **autopilot** — she acts without asking. (Spending money always needs a browser approval — no terminal reply can release it.)
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
Your machine runs the hands — read, write, edit, run. The brain runs in the Imperal Cloud and reasons over your files, your history, and your connected apps through ICNLI. The model is a replaceable proposer at the edge; the kernel resolves, grounds, and decides. Webbee reads your facts. She doesn't invent them.
|
|
44
|
+
|
|
45
|
+
## Links
|
|
46
|
+
|
|
47
|
+
- **Imperal Cloud** — [imperal.io](https://imperal.io)
|
|
48
|
+
- **Docs** — [docs.imperal.io](https://docs.imperal.io)
|
|
49
|
+
- **ICNLI** — the open protocol, CC BY-SA 4.0 — [icnli.org](https://icnli.org)
|
|
50
|
+
- **More from Imperal** — [github.com/imperalcloud](https://github.com/imperalcloud)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
MIT © Imperal, Inc.
|
webbee-0.1.0/install.sh
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Webbee installer — bootstraps uv (which brings its own Python) then installs webbee.
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
if ! command -v uv >/dev/null 2>&1; then
|
|
6
|
+
echo "Installing uv (Python toolchain manager)…"
|
|
7
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
8
|
+
# uv installs to ~/.local/bin or ~/.cargo/bin; make it visible for this run.
|
|
9
|
+
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
echo "Installing webbee…"
|
|
13
|
+
uv tool install webbee
|
|
14
|
+
|
|
15
|
+
echo ""
|
|
16
|
+
echo "✅ webbee installed. Start it with: webbee"
|
|
17
|
+
echo " (if 'webbee' is not found, add uv's tool bin to your PATH: uv tool update-shell)"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "webbee"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
authors = [{ name = "Imperal, Inc.", email = "hello@imperal.io" }]
|
|
10
|
+
dependencies = ["imperal-mcp>=0.4.0", "imperal-sdk>=5.9.2", "httpx>=0.27", "httpx-sse>=0.4", "rich>=13.7", "prompt_toolkit>=3"]
|
|
11
|
+
keywords = ["webbee", "imperal", "icnli", "coding-agent", "ai-agent", "cli", "terminal", "llm"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Topic :: Software Development",
|
|
22
|
+
"Topic :: Utilities",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://imperal.io"
|
|
27
|
+
Documentation = "https://docs.imperal.io"
|
|
28
|
+
Repository = "https://github.com/imperalcloud/webbee-code"
|
|
29
|
+
Issues = "https://github.com/imperalcloud/webbee-code/issues"
|
|
30
|
+
Protocol = "https://icnli.org"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
webbee = "webbee.cli:main"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/webbee"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
pythonpath = ["src"]
|
|
44
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Account:
|
|
8
|
+
signed_in: bool = False
|
|
9
|
+
email: str = ""
|
|
10
|
+
nickname: str = ""
|
|
11
|
+
plan: str = ""
|
|
12
|
+
plan_status: str = ""
|
|
13
|
+
plan_renews: str = ""
|
|
14
|
+
dev_tier: str = ""
|
|
15
|
+
member_since: str = ""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _fmt_month(iso: str) -> str:
|
|
19
|
+
"""'2026-04-27T10:00:00Z' -> 'Apr 2026'; '' on any failure."""
|
|
20
|
+
try:
|
|
21
|
+
return datetime.fromisoformat(iso.replace("Z", "+00:00")).strftime("%b %Y")
|
|
22
|
+
except Exception:
|
|
23
|
+
return ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fmt_day(iso: str) -> str:
|
|
27
|
+
"""'2026-08-01T00:00:00Z' -> '2026-08-01'; '' on any failure."""
|
|
28
|
+
try:
|
|
29
|
+
return datetime.fromisoformat(iso.replace("Z", "+00:00")).strftime("%Y-%m-%d")
|
|
30
|
+
except Exception:
|
|
31
|
+
return ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def _default_get(cfg, token: str, path: str) -> dict:
|
|
35
|
+
import httpx
|
|
36
|
+
async with httpx.AsyncClient(base_url=cfg.api_url, timeout=3.0) as c:
|
|
37
|
+
r = await c.get(path, headers={"Authorization": f"Bearer {token}"})
|
|
38
|
+
r.raise_for_status()
|
|
39
|
+
return r.json()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def fetch_account(cfg, token_provider, *, get=None) -> Account:
|
|
43
|
+
"""Best-effort account summary for the welcome screen. NEVER raises: no
|
|
44
|
+
token or a failed /v1/auth/me -> Account(signed_in=False); a failed
|
|
45
|
+
billing/developer call just omits those fields."""
|
|
46
|
+
try:
|
|
47
|
+
token = await token_provider()
|
|
48
|
+
except Exception:
|
|
49
|
+
return Account(signed_in=False)
|
|
50
|
+
|
|
51
|
+
async def getter(path: str) -> dict:
|
|
52
|
+
if get is not None:
|
|
53
|
+
return await get(path)
|
|
54
|
+
return await _default_get(cfg, token, path)
|
|
55
|
+
|
|
56
|
+
async def _try(path):
|
|
57
|
+
try:
|
|
58
|
+
return await getter(path)
|
|
59
|
+
except Exception:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
me, sub, dev = await asyncio.gather(
|
|
63
|
+
_try("/v1/auth/me"), _try("/v1/billing/subscription"), _try("/v1/developer/profile"))
|
|
64
|
+
if not me:
|
|
65
|
+
return Account(signed_in=False)
|
|
66
|
+
attrs = me.get("attributes") or {}
|
|
67
|
+
sub = sub or {}
|
|
68
|
+
dev = dev or {}
|
|
69
|
+
return Account(
|
|
70
|
+
signed_in=True,
|
|
71
|
+
email=str(me.get("email", "") or ""),
|
|
72
|
+
nickname=str(dev.get("nickname", "") or ""),
|
|
73
|
+
plan=str(sub.get("plan", "") or ""),
|
|
74
|
+
plan_status=str(sub.get("status", "") or ""),
|
|
75
|
+
plan_renews=_fmt_day(str(sub.get("expires_at", "") or "")),
|
|
76
|
+
dev_tier=str(dev.get("tier", "") or attrs.get("developer_tier", "") or ""),
|
|
77
|
+
member_since=_fmt_month(str(me.get("created_at", "") or dev.get("registered_at", "") or "")),
|
|
78
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Webbee Code logo — pyfiglet "big" font (generated). Rendered bee-yellow + centered by render.py.
|
|
2
|
+
WEBBEE_CODE = r"""__ __ _ _ _____ _
|
|
3
|
+
\ \ / / | | | | / ____| | |
|
|
4
|
+
\ \ /\ / /__| |__ | |__ ___ ___ | | ___ __| | ___
|
|
5
|
+
\ \/ \/ / _ \ '_ \| '_ \ / _ \/ _ \ | | / _ \ / _` |/ _ \
|
|
6
|
+
\ /\ / __/ |_) | |_) | __/ __/ | |___| (_) | (_| | __/
|
|
7
|
+
\/ \/ \___|_.__/|_.__/ \___|\___| \_____\___/ \__,_|\___|
|
|
8
|
+
|
|
9
|
+
"""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from webbee import __version__
|
|
6
|
+
from webbee.config import Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
10
|
+
p = argparse.ArgumentParser(prog="webbee", description="Webbee 🐝 — coding agent in your terminal")
|
|
11
|
+
p.add_argument("--version", action="version", version=f"webbee {__version__}")
|
|
12
|
+
p.add_argument("--mode", choices=["default", "plan", "autopilot"], default="default")
|
|
13
|
+
sub = p.add_subparsers(dest="cmd")
|
|
14
|
+
sub.add_parser("login", help="Log in to your Imperal account in the browser")
|
|
15
|
+
sub.add_parser("logout", help="Log out and remove local credentials")
|
|
16
|
+
return p
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv=None) -> None:
|
|
20
|
+
args = build_parser().parse_args(argv)
|
|
21
|
+
cfg = Config.from_env()
|
|
22
|
+
|
|
23
|
+
if args.cmd == "login":
|
|
24
|
+
from imperal_mcp import auth
|
|
25
|
+
print(f"Logged in as {auth.login(cfg)}.")
|
|
26
|
+
return
|
|
27
|
+
if args.cmd == "logout":
|
|
28
|
+
from imperal_mcp import auth
|
|
29
|
+
asyncio.run(auth.logout(cfg))
|
|
30
|
+
print("Logged out.")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
# Default: the polished REPL. Fire a non-blocking update-check first.
|
|
34
|
+
from webbee.repl import run_repl
|
|
35
|
+
try:
|
|
36
|
+
_maybe_print_update_notice()
|
|
37
|
+
asyncio.run(run_repl(cfg, args.mode))
|
|
38
|
+
except KeyboardInterrupt:
|
|
39
|
+
# Ctrl-C during the update-check fetch, or at the read_line() prompt,
|
|
40
|
+
# unwinds here — exit clean, no traceback. (repl.py itself now cancels
|
|
41
|
+
# a Ctrl-C mid-turn internally and returns to the prompt instead of
|
|
42
|
+
# propagating — see run_repl.)
|
|
43
|
+
print("\nBye 🐝")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _maybe_print_update_notice() -> None:
|
|
47
|
+
try:
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
import time
|
|
50
|
+
from webbee.update import check_for_update, default_fetch
|
|
51
|
+
cache = Path(os.path.expanduser("~/.cache/webbee/update.json"))
|
|
52
|
+
notice = check_for_update(__version__, cache_path=cache, now=time.time(), fetch=default_fetch)
|
|
53
|
+
if notice:
|
|
54
|
+
print(notice)
|
|
55
|
+
except Exception:
|
|
56
|
+
pass # update-check must never block or crash startup
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
_MODES = ("default", "plan", "autopilot")
|
|
4
|
+
|
|
5
|
+
_HELP = """Commands:
|
|
6
|
+
/help show this help
|
|
7
|
+
/login sign in to your Imperal account (browser)
|
|
8
|
+
/logout sign out and remove local credentials
|
|
9
|
+
/clear clear the screen + reset session counters
|
|
10
|
+
/mode [default|plan|autopilot] consent mode (no arg — show current)
|
|
11
|
+
/cost (=/usage) tokens + $ cost this session
|
|
12
|
+
/status cwd · git · surface · tokens · version
|
|
13
|
+
/exit (=/quit) quit"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class CommandContext:
|
|
18
|
+
mode: str
|
|
19
|
+
workspace: str
|
|
20
|
+
version: str
|
|
21
|
+
surface: str
|
|
22
|
+
logged_in: bool
|
|
23
|
+
session_tokens: int
|
|
24
|
+
session_cost: float
|
|
25
|
+
git_branch: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class SlashResult:
|
|
30
|
+
handled: bool
|
|
31
|
+
exit: bool = False
|
|
32
|
+
message: str = ""
|
|
33
|
+
action: str = ""
|
|
34
|
+
new_mode: "str | None" = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dispatch(line: str, ctx: CommandContext) -> SlashResult:
|
|
38
|
+
"""Parse one input line. Non-slash lines return handled=False (the REPL
|
|
39
|
+
then sends them to the agent). Slash lines are fully handled here."""
|
|
40
|
+
text = line.strip()
|
|
41
|
+
if not text.startswith("/"):
|
|
42
|
+
return SlashResult(handled=False)
|
|
43
|
+
|
|
44
|
+
parts = text.split()
|
|
45
|
+
cmd, args = parts[0].lower(), parts[1:]
|
|
46
|
+
|
|
47
|
+
if cmd in ("/exit", "/quit"):
|
|
48
|
+
return SlashResult(handled=True, exit=True)
|
|
49
|
+
if cmd == "/help":
|
|
50
|
+
return SlashResult(handled=True, action="help", message=_HELP)
|
|
51
|
+
if cmd == "/login":
|
|
52
|
+
return SlashResult(handled=True, action="login")
|
|
53
|
+
if cmd == "/logout":
|
|
54
|
+
return SlashResult(handled=True, action="logout")
|
|
55
|
+
if cmd == "/clear":
|
|
56
|
+
return SlashResult(handled=True, action="clear", message="Screen cleared, counters reset.")
|
|
57
|
+
if cmd in ("/cost", "/usage"):
|
|
58
|
+
return SlashResult(handled=True, action="cost",
|
|
59
|
+
message=f"This session: {ctx.session_tokens} tokens (~${ctx.session_cost:.4f}). "
|
|
60
|
+
f"LLM turns don't spend credits.")
|
|
61
|
+
if cmd == "/status":
|
|
62
|
+
auth = "signed in" if ctx.logged_in else "not signed in (/login)"
|
|
63
|
+
msg = (f"surface: {ctx.surface} mode: {ctx.mode} {auth}\n"
|
|
64
|
+
f"cwd: {ctx.workspace} git: {ctx.git_branch}\n"
|
|
65
|
+
f"tokens: {ctx.session_tokens} (~${ctx.session_cost:.4f}) webbee v{ctx.version}")
|
|
66
|
+
return SlashResult(handled=True, action="status", message=msg)
|
|
67
|
+
if cmd == "/mode":
|
|
68
|
+
if not args:
|
|
69
|
+
return SlashResult(handled=True, action="mode", new_mode=None,
|
|
70
|
+
message=f"Current mode: {ctx.mode}. Available: {', '.join(_MODES)}.")
|
|
71
|
+
want = args[0].lower()
|
|
72
|
+
if want not in _MODES:
|
|
73
|
+
return SlashResult(handled=True, action="mode", new_mode=None,
|
|
74
|
+
message=f"Unknown mode '{want}'. Available: {', '.join(_MODES)}.")
|
|
75
|
+
return SlashResult(handled=True, action="mode", new_mode=want,
|
|
76
|
+
message=f"Mode → {want}.")
|
|
77
|
+
return SlashResult(handled=True, message=f"Unknown command '{cmd}'. /help for the list.")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
@dataclass(frozen=True)
|
|
5
|
+
class Config:
|
|
6
|
+
api_url: str
|
|
7
|
+
panel_url: str
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def from_env(cls) -> "Config":
|
|
11
|
+
return cls(
|
|
12
|
+
api_url=os.environ.get("IMPERAL_API_URL", "https://auth.imperal.io").rstrip("/"),
|
|
13
|
+
panel_url=os.environ.get("IMPERAL_PANEL_URL", "https://panel.imperal.io").rstrip("/"),
|
|
14
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Protocol, runtime_checkable
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@runtime_checkable
|
|
5
|
+
class TurnSink(Protocol):
|
|
6
|
+
"""Everything a running coding turn tells the UI. The renderer (render.py)
|
|
7
|
+
implements this with Rich; tests implement it with a recording fake.
|
|
8
|
+
session.py imports ONLY this — never Rich — so the SSE loop stays testable."""
|
|
9
|
+
|
|
10
|
+
def tool_start(self, tool: str, args: dict) -> None: ...
|
|
11
|
+
def tool_result(self, tool: str, ok: bool, summary: str) -> None: ...
|
|
12
|
+
async def ask_consent(self, app_id: str, tool: str, args: dict) -> str: ...
|
|
13
|
+
def panel_release(self, panel_url: str, summary: str) -> None: ...
|
|
14
|
+
def progress(self, text: str) -> None: ...
|
|
15
|
+
def usage(self, tokens: int, cost_usd: float) -> None: ...
|
|
16
|
+
def plan_blocked(self, tool: str) -> None: ...
|
|
17
|
+
def user_echo(self, text: str) -> None: ...
|