webbee 0.1.18__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {webbee-0.1.18 → webbee-0.2.0}/CHANGELOG.md +10 -0
- {webbee-0.1.18 → webbee-0.2.0}/PKG-INFO +1 -1
- {webbee-0.1.18 → webbee-0.2.0}/pyproject.toml +1 -1
- webbee-0.2.0/src/webbee/__init__.py +1 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/cli.py +6 -1
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/render.py +20 -5
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/repl.py +2 -2
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_repl.py +22 -6
- webbee-0.1.18/src/webbee/__init__.py +0 -1
- {webbee-0.1.18 → webbee-0.2.0}/.github/workflows/publish.yml +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/.gitignore +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/LICENSE +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/README.md +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/install.sh +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/account.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/banner_art.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/clipboard.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/commands.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/config.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/details.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/events.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/frames.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/embed.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/graph.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/models.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/query.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/service.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/store.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/intel/watch.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/output_pane.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/repo.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/session.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/sessions.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/stream.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/tools.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/tui.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/src/webbee/update.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/__init__.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_account.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_cli.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_clipboard.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_commands.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_config.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_details.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_events.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_freeze_fix.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_intel_chunker.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_intel_embed.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_intel_graph.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_intel_indexer.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_intel_query.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_intel_service.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_intel_store.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_intel_vectors.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_intel_watch.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_marathon.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_packaging.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_render.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_repo.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_session.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_sessions.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_stream.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_tools.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_tui.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_tui_hardening.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_update.py +0 -0
- {webbee-0.1.18 → webbee-0.2.0}/tests/test_version.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- **Marathon is now the default.** A task you type runs to completion on its own
|
|
6
|
+
— it plans, edits, runs, and verifies across as many steps as it needs, and
|
|
7
|
+
only stops when the goal is done, you stop it, or your credits run low (it
|
|
8
|
+
pauses, it doesn't fail — top up and it resumes). No more fixed per-run step
|
|
9
|
+
limit on the default path.
|
|
10
|
+
- **`--once` (alias `--no-marathon`)** runs a single bounded coding turn instead,
|
|
11
|
+
for a quick one-shot.
|
|
12
|
+
|
|
3
13
|
## 0.1.14
|
|
4
14
|
|
|
5
15
|
- **Esc/Ctrl-C now actually stops the turn — server-side, not just locally.**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -15,6 +15,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
15
15
|
help="Launch a long-horizon marathon toward GOAL (runs autonomously "
|
|
16
16
|
"until the goal is met, verified by the project's own test command).",
|
|
17
17
|
)
|
|
18
|
+
p.add_argument(
|
|
19
|
+
"--once", "--no-marathon", dest="once", action="store_true",
|
|
20
|
+
help="Run a single bounded coding turn (stops at the step limit) instead "
|
|
21
|
+
"of the default self-driving marathon.",
|
|
22
|
+
)
|
|
18
23
|
sub = p.add_subparsers(dest="cmd")
|
|
19
24
|
sub.add_parser("login", help="Log in to your Imperal account in the browser")
|
|
20
25
|
sub.add_parser("logout", help="Log out and remove local credentials")
|
|
@@ -63,7 +68,7 @@ def main(argv=None) -> None:
|
|
|
63
68
|
from webbee.repl import run_repl
|
|
64
69
|
try:
|
|
65
70
|
_maybe_print_update_notice()
|
|
66
|
-
asyncio.run(run_repl(cfg, args.mode))
|
|
71
|
+
asyncio.run(run_repl(cfg, args.mode, once=args.once))
|
|
67
72
|
except KeyboardInterrupt:
|
|
68
73
|
# Ctrl-C during the update-check fetch, or at the read_line() prompt,
|
|
69
74
|
# unwinds here — exit clean, no traceback. (repl.py itself now cancels
|
|
@@ -3,11 +3,23 @@ import time
|
|
|
3
3
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
from rich.markdown import Markdown
|
|
6
|
+
from rich.padding import Padding
|
|
6
7
|
from rich.panel import Panel
|
|
7
8
|
from rich.text import Text
|
|
8
9
|
|
|
9
10
|
from webbee.banner_art import WEBBEE_CODE
|
|
10
11
|
|
|
12
|
+
_GUTTER = 2 # left margin (cols) — the single consistent transcript gutter.
|
|
13
|
+
# Chrome lines prefix a 2-space string (" "); block renderables
|
|
14
|
+
# (Markdown answer, Panels, Table) are wrapped in _pad() to match,
|
|
15
|
+
# so NOTHING renders flush against the screen edge.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _pad(renderable):
|
|
19
|
+
"""Indent a block renderable by the transcript gutter so its left edge
|
|
20
|
+
lines up with the 2-space chrome (' 🐝 Webbee', ' 13.9s · …', the ❯ bar)."""
|
|
21
|
+
return Padding(renderable, (0, 0, 0, _GUTTER))
|
|
22
|
+
|
|
11
23
|
_ICON = {"read_file": "📖", "grep": "🔎", "glob": "🗂️", "write_file": "✎",
|
|
12
24
|
"edit_file": "🔧", "bash": "⚡"}
|
|
13
25
|
_BEE = "yellow" # bee-yellow brand accent — logo / 🐝 / notes / busy dot ONLY
|
|
@@ -128,6 +140,9 @@ class RichSink:
|
|
|
128
140
|
for label, value in rows:
|
|
129
141
|
self.console.print(Text.assemble((pad + label.ljust(14), "dim"), (value, "white")))
|
|
130
142
|
self.console.print()
|
|
143
|
+
tip = "Type a task and Webbee runs it to completion · pauses if credits run low · --once = single turn"
|
|
144
|
+
self.console.print(Text(tip.center(w), style="dim"))
|
|
145
|
+
self.console.print()
|
|
131
146
|
self.console.print(Text("/help · Ctrl-D to exit".center(w), style="dim"))
|
|
132
147
|
self.console.print()
|
|
133
148
|
self._nudge()
|
|
@@ -149,7 +164,7 @@ class RichSink:
|
|
|
149
164
|
if final_text:
|
|
150
165
|
self.console.print() # separation before the focus block
|
|
151
166
|
self.console.print(Text(" 🐝 Webbee", style=f"bold {_BEE}"))
|
|
152
|
-
self.console.print(Markdown(final_text))
|
|
167
|
+
self.console.print(_pad(Markdown(final_text))) # body aligns to the same 2-col gutter as the header
|
|
153
168
|
elapsed = self._elapsed()
|
|
154
169
|
self.session_tokens += self.tokens
|
|
155
170
|
self.session_credits += self.credits
|
|
@@ -246,7 +261,7 @@ class RichSink:
|
|
|
246
261
|
(f" {panel_url}\n", f"bold {_ACCENT}"),
|
|
247
262
|
("Then ask again — you weren't charged.", "dim"),
|
|
248
263
|
)
|
|
249
|
-
self.console.print(Panel(body, title="💳 This costs money", border_style="magenta"))
|
|
264
|
+
self.console.print(_pad(Panel(body, title="💳 This costs money", border_style="magenta")))
|
|
250
265
|
self._nudge()
|
|
251
266
|
|
|
252
267
|
def login_prompt(self, user_code: str, url: str) -> None:
|
|
@@ -261,7 +276,7 @@ class RichSink:
|
|
|
261
276
|
(f" {user_code}\n\n", f"bold {_BEE}"),
|
|
262
277
|
("Waiting for you to authorize…", "dim"),
|
|
263
278
|
)
|
|
264
|
-
self.console.print(Panel(body, title="🐝 Connect this terminal", border_style=_BEE))
|
|
279
|
+
self.console.print(_pad(Panel(body, title="🐝 Connect this terminal", border_style=_BEE)))
|
|
265
280
|
self._nudge()
|
|
266
281
|
|
|
267
282
|
def sessions_table(self, sessions) -> None:
|
|
@@ -285,7 +300,7 @@ class RichSink:
|
|
|
285
300
|
seen = str(s.get("last_seen_at") or "")[:16].replace("T", " ")
|
|
286
301
|
here = Text("this device", style=f"bold {_BEE}") if s.get("current") else Text("")
|
|
287
302
|
t.add_row(str(i), label, ip, seen, here)
|
|
288
|
-
self.console.print(t)
|
|
303
|
+
self.console.print(_pad(t))
|
|
289
304
|
self.console.print(Text(" /sessions revoke <#> · /logout-others", style="dim"))
|
|
290
305
|
self._nudge()
|
|
291
306
|
|
|
@@ -302,7 +317,7 @@ class RichSink:
|
|
|
302
317
|
f"{(detail.get('duration_ms', 0) or 0) / 1000:.1f}s "
|
|
303
318
|
f"trace {detail.get('trace_id', '')}")
|
|
304
319
|
body = f"{head}\n\nargs:\n{_prev(detail.get('args'))}\n\nresult:\n{_prev(detail.get('result'))}"
|
|
305
|
-
self.console.print(Panel(body, border_style="dim", title="step detail", title_align="left"))
|
|
320
|
+
self.console.print(_pad(Panel(body, border_style="dim", title="step detail", title_align="left")))
|
|
306
321
|
self._nudge()
|
|
307
322
|
|
|
308
323
|
def progress(self, text: str) -> None:
|
|
@@ -91,7 +91,7 @@ def _default_intel_factory(cfg, workspace: str):
|
|
|
91
91
|
return IntelService(root, compute_repo_key(root), cache_dir=cfg.cache_dir)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
async def run_repl(cfg, mode: str = "default", *, sink=None, read_line=input,
|
|
94
|
+
async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None, read_line=input,
|
|
95
95
|
agent_factory=None, auth=None, account_fetcher=None,
|
|
96
96
|
sessions_client=None, intel_factory=None) -> None:
|
|
97
97
|
"""Interactive coding REPL. Production (a real tty, no injected sink) runs
|
|
@@ -226,7 +226,7 @@ async def run_repl(cfg, mode: str = "default", *, sink=None, read_line=input,
|
|
|
226
226
|
_sink.user_echo(line)
|
|
227
227
|
_sink.begin_turn()
|
|
228
228
|
try:
|
|
229
|
-
text = await agent.run(line, _sink)
|
|
229
|
+
text = await agent.run(line, _sink, marathon=not once, goal=(line if not once else ""))
|
|
230
230
|
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
231
231
|
_sink.abort()
|
|
232
232
|
_sink.note("Interrupted.")
|
|
@@ -30,9 +30,10 @@ class FakeSink:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class FakeAgent:
|
|
33
|
-
def __init__(self): self.tasks = []; self.mode = "default"
|
|
34
|
-
async def run(self, task, sink):
|
|
33
|
+
def __init__(self): self.tasks = []; self.mode = "default"; self.runs = []
|
|
34
|
+
async def run(self, task, sink, *, marathon=False, goal=""):
|
|
35
35
|
self.tasks.append(task)
|
|
36
|
+
self.runs.append({"task": task, "marathon": marathon, "goal": goal})
|
|
36
37
|
return f"answer:{task}"
|
|
37
38
|
|
|
38
39
|
|
|
@@ -95,7 +96,8 @@ def _run(**kw):
|
|
|
95
96
|
cfg = Config(api_url="http://x", panel_url="http://p")
|
|
96
97
|
sink = kw.pop("sink", FakeSink())
|
|
97
98
|
agent = kw.pop("agent", FakeAgent())
|
|
98
|
-
|
|
99
|
+
once = kw.pop("once", False)
|
|
100
|
+
asyncio.run(run_repl(cfg, "default", once=once, sink=sink, agent_factory=lambda c, tp, ws, m: agent,
|
|
99
101
|
read_line=kw.pop("read_line"), auth=kw.pop("auth", FakeAuth()),
|
|
100
102
|
account_fetcher=kw.pop("account_fetcher", _fake_account_fetcher),
|
|
101
103
|
sessions_client=kw.pop("sessions_client", FakeSessions()),
|
|
@@ -109,6 +111,20 @@ def test_task_is_sent_to_agent_and_answer_rendered():
|
|
|
109
111
|
assert sink.turns == ["answer:исправь баг"]
|
|
110
112
|
|
|
111
113
|
|
|
114
|
+
def test_interactive_default_is_marathon():
|
|
115
|
+
# Marathon is the default: a typed task self-drives (marathon=True) with the
|
|
116
|
+
# task carried as the goal.
|
|
117
|
+
sink, agent = _run(read_line=_lines("build a thing", "/exit"))
|
|
118
|
+
assert agent.runs and agent.runs[0]["marathon"] is True
|
|
119
|
+
assert agent.runs[0]["goal"] == "build a thing"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_once_flag_uses_bounded_coding():
|
|
123
|
+
# --once opts back into the bounded, non-marathon coding turn.
|
|
124
|
+
sink, agent = _run(read_line=_lines("build a thing", "/exit"), once=True)
|
|
125
|
+
assert agent.runs and agent.runs[0]["marathon"] is False
|
|
126
|
+
|
|
127
|
+
|
|
112
128
|
def test_exit_command_stops_loop():
|
|
113
129
|
sink, agent = _run(read_line=_lines("/exit"))
|
|
114
130
|
assert agent.tasks == []
|
|
@@ -133,7 +149,7 @@ def test_logout_command_calls_auth():
|
|
|
133
149
|
|
|
134
150
|
def test_agent_error_is_swallowed_and_loop_continues():
|
|
135
151
|
class RaisingAgent(FakeAgent):
|
|
136
|
-
async def run(self, task, sink):
|
|
152
|
+
async def run(self, task, sink, *, marathon=False, goal=""):
|
|
137
153
|
self.tasks.append(task)
|
|
138
154
|
raise RuntimeError("boom")
|
|
139
155
|
|
|
@@ -204,7 +220,7 @@ def test_clear_command_clears_sink():
|
|
|
204
220
|
|
|
205
221
|
def test_ctrl_c_mid_turn_aborts_and_returns_to_prompt():
|
|
206
222
|
class InterruptingAgent(FakeAgent):
|
|
207
|
-
async def run(self, task, sink):
|
|
223
|
+
async def run(self, task, sink, *, marathon=False, goal=""):
|
|
208
224
|
self.tasks.append(task)
|
|
209
225
|
raise KeyboardInterrupt
|
|
210
226
|
|
|
@@ -309,7 +325,7 @@ class _SpyAgent:
|
|
|
309
325
|
self.mode = mode
|
|
310
326
|
self.steps = []
|
|
311
327
|
|
|
312
|
-
async def run(self, task, sink):
|
|
328
|
+
async def run(self, task, sink, *, marathon=False, goal=""):
|
|
313
329
|
return f"answer:{task}"
|
|
314
330
|
|
|
315
331
|
async def stop(self): ...
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.18"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|