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.
@@ -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 }}
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .venv/
6
+ venv/
7
+ build/
8
+ dist/
9
+ .DS_Store
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/webbee.svg)](https://pypi.org/project/webbee/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/webbee.svg)](https://pypi.org/project/webbee/)
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
38
+ [![Docs](https://img.shields.io/badge/docs-imperal.io-00afd7.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/webbee.svg)](https://pypi.org/project/webbee/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/webbee.svg)](https://pypi.org/project/webbee/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
6
+ [![Docs](https://img.shields.io/badge/docs-imperal.io-00afd7.svg)](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.
@@ -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: ...