bumblebee-cli 0.1.1 → 0.2.0
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.
- package/README.md +49 -47
- package/bin/bb.mjs +132 -132
- package/package.json +28 -28
- package/python/bb_cli/__main__.py +3 -0
- package/python/bb_cli/api_client.py +7 -0
- package/python/bb_cli/commands/agent.py +2287 -1030
- package/python/bb_cli/commands/auth.py +79 -79
- package/python/bb_cli/commands/board.py +47 -47
- package/python/bb_cli/commands/comment.py +34 -34
- package/python/bb_cli/commands/daemon.py +153 -0
- package/python/bb_cli/commands/init.py +83 -62
- package/python/bb_cli/commands/item.py +192 -192
- package/python/bb_cli/commands/project.py +175 -111
- package/python/bb_cli/config.py +136 -136
- package/python/bb_cli/main.py +44 -44
- package/python/bb_cli/progress.py +117 -0
- package/python/bb_cli/streaming.py +168 -0
- package/python/pyproject.toml +1 -1
- package/python/{bb_cli/bumblebee_cli.egg-info/requires.txt → requirements.txt} +4 -4
- package/scripts/build.sh +20 -20
- package/scripts/postinstall.mjs +146 -146
- package/python/bb_cli/bumblebee_cli.egg-info/PKG-INFO +0 -9
- package/python/bb_cli/bumblebee_cli.egg-info/SOURCES.txt +0 -21
- package/python/bb_cli/bumblebee_cli.egg-info/dependency_links.txt +0 -1
- package/python/bb_cli/bumblebee_cli.egg-info/entry_points.txt +0 -2
- package/python/bb_cli/bumblebee_cli.egg-info/top_level.txt +0 -5
- package/python/bb_cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/agent.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/auth.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/board.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/comment.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/init.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/item.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/label.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/project.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/sprint.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/story.cpython-313.pyc +0 -0
- package/python/bb_cli/commands/__pycache__/task.cpython-313.pyc +0 -0
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich import print as rprint
|
|
3
|
-
|
|
4
|
-
from ..api_client import api_get, api_post
|
|
5
|
-
from ..config import get_api_url, get_project_path, is_local_config_active, load_config, save_config
|
|
6
|
-
|
|
7
|
-
app = typer.Typer(help="Authentication commands")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@app.command()
|
|
11
|
-
def login(
|
|
12
|
-
email: str = typer.Option(..., prompt=True),
|
|
13
|
-
password: str = typer.Option(..., prompt=True, hide_input=True),
|
|
14
|
-
):
|
|
15
|
-
"""Log in to Bumblebee API."""
|
|
16
|
-
try:
|
|
17
|
-
data = api_post("/auth/login", json={"email": email, "password": password})
|
|
18
|
-
cfg = load_config()
|
|
19
|
-
cfg["token"] = data["access_token"]
|
|
20
|
-
save_config(cfg)
|
|
21
|
-
rprint("[green]Logged in successfully.[/green]")
|
|
22
|
-
except Exception as e:
|
|
23
|
-
rprint(f"[red]Login failed: {e}[/red]")
|
|
24
|
-
raise typer.Exit(1)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@app.command()
|
|
28
|
-
def register(
|
|
29
|
-
email: str = typer.Option(..., prompt=True),
|
|
30
|
-
username: str = typer.Option(..., prompt=True),
|
|
31
|
-
password: str = typer.Option(..., prompt=True, hide_input=True, confirmation_prompt=True),
|
|
32
|
-
):
|
|
33
|
-
"""Register a new Bumblebee account."""
|
|
34
|
-
try:
|
|
35
|
-
api_post("/auth/register", json={"email": email, "username": username, "password": password})
|
|
36
|
-
rprint("[green]Registered! You can now run [bold]bb login[/bold].[/green]")
|
|
37
|
-
except Exception as e:
|
|
38
|
-
rprint(f"[red]Registration failed: {e}[/red]")
|
|
39
|
-
raise typer.Exit(1)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@app.command()
|
|
43
|
-
def logout():
|
|
44
|
-
"""Clear saved credentials."""
|
|
45
|
-
cfg = load_config()
|
|
46
|
-
cfg.pop("token", None)
|
|
47
|
-
save_config(cfg)
|
|
48
|
-
rprint("[yellow]Logged out.[/yellow]")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@app.command()
|
|
52
|
-
def whoami():
|
|
53
|
-
"""Show current user info."""
|
|
54
|
-
try:
|
|
55
|
-
user = api_get("/auth/me")
|
|
56
|
-
rprint(f"[bold]{user['username']}[/bold] ({user['email']})")
|
|
57
|
-
except Exception:
|
|
58
|
-
rprint("[red]Not logged in.[/red]")
|
|
59
|
-
raise typer.Exit(1)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@app.command(name="config")
|
|
63
|
-
def show_config():
|
|
64
|
-
"""Show current CLI configuration."""
|
|
65
|
-
cfg = load_config()
|
|
66
|
-
slug = cfg.get("current_project")
|
|
67
|
-
config_source = "local (.bumblebee/)" if is_local_config_active() else "global (~/.bumblebee/)"
|
|
68
|
-
rprint(f"Config source: {config_source}")
|
|
69
|
-
rprint(f"API URL: {get_api_url()}")
|
|
70
|
-
rprint(f"Logged in: {'Yes' if cfg.get('token') else 'No'}")
|
|
71
|
-
rprint(f"Current project: {slug or 'None'}")
|
|
72
|
-
if slug:
|
|
73
|
-
path = get_project_path(slug)
|
|
74
|
-
rprint(f"Source path: {path or '[dim]not linked[/dim]'}")
|
|
75
|
-
projects = cfg.get("projects", {})
|
|
76
|
-
if projects:
|
|
77
|
-
rprint("\n[bold]Project paths:[/bold]")
|
|
78
|
-
for name, info in projects.items():
|
|
79
|
-
rprint(f" {name} -> {info.get('path', '?')}")
|
|
1
|
+
import typer
|
|
2
|
+
from rich import print as rprint
|
|
3
|
+
|
|
4
|
+
from ..api_client import api_get, api_post
|
|
5
|
+
from ..config import get_api_url, get_project_path, is_local_config_active, load_config, save_config
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(help="Authentication commands")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command()
|
|
11
|
+
def login(
|
|
12
|
+
email: str = typer.Option(..., prompt=True),
|
|
13
|
+
password: str = typer.Option(..., prompt=True, hide_input=True),
|
|
14
|
+
):
|
|
15
|
+
"""Log in to Bumblebee API."""
|
|
16
|
+
try:
|
|
17
|
+
data = api_post("/auth/login", json={"email": email, "password": password})
|
|
18
|
+
cfg = load_config()
|
|
19
|
+
cfg["token"] = data["access_token"]
|
|
20
|
+
save_config(cfg)
|
|
21
|
+
rprint("[green]Logged in successfully.[/green]")
|
|
22
|
+
except Exception as e:
|
|
23
|
+
rprint(f"[red]Login failed: {e}[/red]")
|
|
24
|
+
raise typer.Exit(1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
def register(
|
|
29
|
+
email: str = typer.Option(..., prompt=True),
|
|
30
|
+
username: str = typer.Option(..., prompt=True),
|
|
31
|
+
password: str = typer.Option(..., prompt=True, hide_input=True, confirmation_prompt=True),
|
|
32
|
+
):
|
|
33
|
+
"""Register a new Bumblebee account."""
|
|
34
|
+
try:
|
|
35
|
+
api_post("/auth/register", json={"email": email, "username": username, "password": password})
|
|
36
|
+
rprint("[green]Registered! You can now run [bold]bb login[/bold].[/green]")
|
|
37
|
+
except Exception as e:
|
|
38
|
+
rprint(f"[red]Registration failed: {e}[/red]")
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command()
|
|
43
|
+
def logout():
|
|
44
|
+
"""Clear saved credentials."""
|
|
45
|
+
cfg = load_config()
|
|
46
|
+
cfg.pop("token", None)
|
|
47
|
+
save_config(cfg)
|
|
48
|
+
rprint("[yellow]Logged out.[/yellow]")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command()
|
|
52
|
+
def whoami():
|
|
53
|
+
"""Show current user info."""
|
|
54
|
+
try:
|
|
55
|
+
user = api_get("/auth/me")
|
|
56
|
+
rprint(f"[bold]{user['username']}[/bold] ({user['email']})")
|
|
57
|
+
except Exception:
|
|
58
|
+
rprint("[red]Not logged in.[/red]")
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command(name="config")
|
|
63
|
+
def show_config():
|
|
64
|
+
"""Show current CLI configuration."""
|
|
65
|
+
cfg = load_config()
|
|
66
|
+
slug = cfg.get("current_project")
|
|
67
|
+
config_source = "local (.bumblebee/)" if is_local_config_active() else "global (~/.bumblebee/)"
|
|
68
|
+
rprint(f"Config source: {config_source}")
|
|
69
|
+
rprint(f"API URL: {get_api_url()}")
|
|
70
|
+
rprint(f"Logged in: {'Yes' if cfg.get('token') else 'No'}")
|
|
71
|
+
rprint(f"Current project: {slug or 'None'}")
|
|
72
|
+
if slug:
|
|
73
|
+
path = get_project_path(slug)
|
|
74
|
+
rprint(f"Source path: {path or '[dim]not linked[/dim]'}")
|
|
75
|
+
projects = cfg.get("projects", {})
|
|
76
|
+
if projects:
|
|
77
|
+
rprint("\n[bold]Project paths:[/bold]")
|
|
78
|
+
for name, info in projects.items():
|
|
79
|
+
rprint(f" {name} -> {info.get('path', '?')}")
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich import print as rprint
|
|
3
|
-
from rich.columns import Columns
|
|
4
|
-
from rich.panel import Panel
|
|
5
|
-
|
|
6
|
-
from ..api_client import api_get
|
|
7
|
-
from ..config import get_current_project
|
|
8
|
-
|
|
9
|
-
app = typer.Typer(help="Kanban board view")
|
|
10
|
-
|
|
11
|
-
COLUMNS = ["open", "in_progress", "in_review", "resolved", "closed"]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@app.callback(invoke_without_command=True)
|
|
15
|
-
def board(
|
|
16
|
-
type: str = typer.Option(None, "-t", "--type", help="Filter by type (story, task, etc.)"),
|
|
17
|
-
):
|
|
18
|
-
"""Show kanban board for current project work items."""
|
|
19
|
-
slug = get_current_project()
|
|
20
|
-
if not slug:
|
|
21
|
-
rprint("[red]No project selected.[/red]")
|
|
22
|
-
raise typer.Exit(1)
|
|
23
|
-
|
|
24
|
-
params = {}
|
|
25
|
-
if type:
|
|
26
|
-
params["type"] = type
|
|
27
|
-
items = api_get(f"/api/projects/{slug}/work-items", params=params)
|
|
28
|
-
|
|
29
|
-
panels = []
|
|
30
|
-
for col in COLUMNS:
|
|
31
|
-
col_items = [i for i in items if i["status"] == col]
|
|
32
|
-
content = ""
|
|
33
|
-
for i in col_items:
|
|
34
|
-
key = i.get("key") or f"#{i['number']}"
|
|
35
|
-
prio = i.get("priority", "")
|
|
36
|
-
assignee = i.get("assignee") or ""
|
|
37
|
-
content += f"[bold]{key}[/bold] [{i['type']}] {i['title']}\n"
|
|
38
|
-
if prio or assignee:
|
|
39
|
-
content += f" [dim]{prio}[/dim]"
|
|
40
|
-
if assignee:
|
|
41
|
-
content += f" -> {assignee}"
|
|
42
|
-
content += "\n"
|
|
43
|
-
if not content:
|
|
44
|
-
content = "[dim]Empty[/dim]"
|
|
45
|
-
panels.append(Panel(content.strip(), title=col.replace("_", " ").title(), expand=True))
|
|
46
|
-
|
|
47
|
-
rprint(Columns(panels, equal=True, expand=True))
|
|
1
|
+
import typer
|
|
2
|
+
from rich import print as rprint
|
|
3
|
+
from rich.columns import Columns
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
|
|
6
|
+
from ..api_client import api_get
|
|
7
|
+
from ..config import get_current_project
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Kanban board view")
|
|
10
|
+
|
|
11
|
+
COLUMNS = ["open", "in_progress", "in_review", "resolved", "closed"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.callback(invoke_without_command=True)
|
|
15
|
+
def board(
|
|
16
|
+
type: str = typer.Option(None, "-t", "--type", help="Filter by type (story, task, etc.)"),
|
|
17
|
+
):
|
|
18
|
+
"""Show kanban board for current project work items."""
|
|
19
|
+
slug = get_current_project()
|
|
20
|
+
if not slug:
|
|
21
|
+
rprint("[red]No project selected.[/red]")
|
|
22
|
+
raise typer.Exit(1)
|
|
23
|
+
|
|
24
|
+
params = {}
|
|
25
|
+
if type:
|
|
26
|
+
params["type"] = type
|
|
27
|
+
items = api_get(f"/api/projects/{slug}/work-items", params=params)
|
|
28
|
+
|
|
29
|
+
panels = []
|
|
30
|
+
for col in COLUMNS:
|
|
31
|
+
col_items = [i for i in items if i["status"] == col]
|
|
32
|
+
content = ""
|
|
33
|
+
for i in col_items:
|
|
34
|
+
key = i.get("key") or f"#{i['number']}"
|
|
35
|
+
prio = i.get("priority", "")
|
|
36
|
+
assignee = i.get("assignee") or ""
|
|
37
|
+
content += f"[bold]{key}[/bold] [{i['type']}] {i['title']}\n"
|
|
38
|
+
if prio or assignee:
|
|
39
|
+
content += f" [dim]{prio}[/dim]"
|
|
40
|
+
if assignee:
|
|
41
|
+
content += f" -> {assignee}"
|
|
42
|
+
content += "\n"
|
|
43
|
+
if not content:
|
|
44
|
+
content = "[dim]Empty[/dim]"
|
|
45
|
+
panels.append(Panel(content.strip(), title=col.replace("_", " ").title(), expand=True))
|
|
46
|
+
|
|
47
|
+
rprint(Columns(panels, equal=True, expand=True))
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich import print as rprint
|
|
3
|
-
|
|
4
|
-
from ..api_client import api_get, api_post
|
|
5
|
-
from .item import _require_project, _resolve_item
|
|
6
|
-
|
|
7
|
-
app = typer.Typer(help="Comment management")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@app.command(name="list")
|
|
11
|
-
def list_comments(id_or_number: str = typer.Argument(..., help="Work item UUID, number, or KEY-number")):
|
|
12
|
-
"""List comments on a work item."""
|
|
13
|
-
slug = _require_project()
|
|
14
|
-
item = _resolve_item(slug, id_or_number)
|
|
15
|
-
comments = api_get(f"/api/work-items/{item['id']}/comments")
|
|
16
|
-
if not comments:
|
|
17
|
-
rprint("[dim]No comments yet.[/dim]")
|
|
18
|
-
return
|
|
19
|
-
for c in comments:
|
|
20
|
-
type_tag = f" [{c['type']}]" if c.get("type") and c["type"] != "discussion" else ""
|
|
21
|
-
rprint(f"[bold]{c['author']}[/bold]{type_tag}: {c['body']}")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@app.command()
|
|
25
|
-
def add(
|
|
26
|
-
id_or_number: str = typer.Argument(..., help="Work item UUID, number, or KEY-number"),
|
|
27
|
-
body: str = typer.Argument(...),
|
|
28
|
-
type: str = typer.Option("discussion", "-t", "--type", help="Comment type: discussion, investigation, proposal, review, agent_output"),
|
|
29
|
-
):
|
|
30
|
-
"""Add a comment to a work item."""
|
|
31
|
-
slug = _require_project()
|
|
32
|
-
item = _resolve_item(slug, id_or_number)
|
|
33
|
-
api_post(f"/api/work-items/{item['id']}/comments", json={"body": body, "type": type})
|
|
34
|
-
rprint("[green]Comment added.[/green]")
|
|
1
|
+
import typer
|
|
2
|
+
from rich import print as rprint
|
|
3
|
+
|
|
4
|
+
from ..api_client import api_get, api_post
|
|
5
|
+
from .item import _require_project, _resolve_item
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(help="Comment management")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command(name="list")
|
|
11
|
+
def list_comments(id_or_number: str = typer.Argument(..., help="Work item UUID, number, or KEY-number")):
|
|
12
|
+
"""List comments on a work item."""
|
|
13
|
+
slug = _require_project()
|
|
14
|
+
item = _resolve_item(slug, id_or_number)
|
|
15
|
+
comments = api_get(f"/api/work-items/{item['id']}/comments")
|
|
16
|
+
if not comments:
|
|
17
|
+
rprint("[dim]No comments yet.[/dim]")
|
|
18
|
+
return
|
|
19
|
+
for c in comments:
|
|
20
|
+
type_tag = f" [{c['type']}]" if c.get("type") and c["type"] != "discussion" else ""
|
|
21
|
+
rprint(f"[bold]{c['author']}[/bold]{type_tag}: {c['body']}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command()
|
|
25
|
+
def add(
|
|
26
|
+
id_or_number: str = typer.Argument(..., help="Work item UUID, number, or KEY-number"),
|
|
27
|
+
body: str = typer.Argument(...),
|
|
28
|
+
type: str = typer.Option("discussion", "-t", "--type", help="Comment type: discussion, investigation, proposal, review, agent_output"),
|
|
29
|
+
):
|
|
30
|
+
"""Add a comment to a work item."""
|
|
31
|
+
slug = _require_project()
|
|
32
|
+
item = _resolve_item(slug, id_or_number)
|
|
33
|
+
api_post(f"/api/work-items/{item['id']}/comments", json={"body": body, "type": type})
|
|
34
|
+
rprint("[green]Comment added.[/green]")
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Long-running daemon that picks up web-initiated agent requests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich import print as rprint
|
|
13
|
+
|
|
14
|
+
from ..api_client import api_get, api_post
|
|
15
|
+
from ..config import get_api_url, get_current_project, get_project_path, get_token
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(help="Agent daemon for web-initiated sessions")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _daemon_id() -> str:
|
|
21
|
+
"""Generate a unique daemon identifier."""
|
|
22
|
+
return f"{platform.node()}-{id(sys)}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _poll_pending_sessions(slug: str) -> list[dict]:
|
|
26
|
+
"""Poll API for pending (web-initiated) sessions."""
|
|
27
|
+
try:
|
|
28
|
+
sessions = api_get("/api/agent-sessions", params={"project_slug": slug})
|
|
29
|
+
return [s for s in sessions if s.get("status") == "pending" and not s.get("claimed_by")]
|
|
30
|
+
except Exception:
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _claim_session(session_id: int, daemon_id: str) -> dict | None:
|
|
35
|
+
"""Attempt to claim a pending session. Returns session or None if already claimed."""
|
|
36
|
+
try:
|
|
37
|
+
return api_post(
|
|
38
|
+
f"/api/agent-sessions/{session_id}/claim",
|
|
39
|
+
params={"daemon_id": daemon_id},
|
|
40
|
+
)
|
|
41
|
+
except Exception:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _run_session(slug: str, project_path: str, session: dict):
|
|
46
|
+
"""Execute the work for a claimed session."""
|
|
47
|
+
from .agent import _execute_one, _suggest_one, _verify_one
|
|
48
|
+
|
|
49
|
+
session_id = session["id"]
|
|
50
|
+
work_item_id = session.get("work_item_id")
|
|
51
|
+
phase = session.get("phase") or "execute"
|
|
52
|
+
|
|
53
|
+
if not work_item_id:
|
|
54
|
+
rprint(f" [yellow]Session {session_id}: no work_item_id, skipping.[/yellow]")
|
|
55
|
+
api_post(f"/api/agent-sessions/{session_id}/complete", json={
|
|
56
|
+
"status": "failed", "error": "No work item specified",
|
|
57
|
+
})
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Resolve item number for the worker functions
|
|
61
|
+
try:
|
|
62
|
+
item = api_get(f"/api/work-items/{work_item_id}")
|
|
63
|
+
id_or_number = item.get("key") or str(item["number"])
|
|
64
|
+
except Exception as e:
|
|
65
|
+
rprint(f" [red]Session {session_id}: failed to resolve item {work_item_id}: {e}[/red]")
|
|
66
|
+
api_post(f"/api/agent-sessions/{session_id}/complete", json={
|
|
67
|
+
"status": "failed", "error": str(e),
|
|
68
|
+
})
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
rprint(f" [cyan]Running {phase} for {id_or_number}...[/cyan]")
|
|
72
|
+
|
|
73
|
+
if phase == "suggest":
|
|
74
|
+
result = _suggest_one(slug, project_path, id_or_number)
|
|
75
|
+
elif phase == "verify":
|
|
76
|
+
# verify() is a CLI command, we use suggest as fallback
|
|
77
|
+
result = _suggest_one(slug, project_path, id_or_number)
|
|
78
|
+
else:
|
|
79
|
+
result = _execute_one(slug, project_path, id_or_number)
|
|
80
|
+
|
|
81
|
+
status = "ok" if result.get("status") == "ok" else "failed"
|
|
82
|
+
color = "green" if status == "ok" else "red"
|
|
83
|
+
rprint(f" [{color}]{id_or_number}: {status}[/{color}]"
|
|
84
|
+
+ (f" — {result.get('error', '')}" if status != "ok" else ""))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
def start(
|
|
89
|
+
poll_interval: int = typer.Option(5, "--poll", "-p", help="Polling interval in seconds"),
|
|
90
|
+
):
|
|
91
|
+
"""Start the agent daemon — polls for web-initiated sessions and executes them.
|
|
92
|
+
|
|
93
|
+
Run this in a terminal to enable the web dashboard to spawn agent sessions
|
|
94
|
+
on your local machine.
|
|
95
|
+
"""
|
|
96
|
+
slug = get_current_project()
|
|
97
|
+
if not slug:
|
|
98
|
+
rprint("[red]No project configured. Run 'bb init' or 'bb project use <slug>' first.[/red]")
|
|
99
|
+
raise typer.Exit(1)
|
|
100
|
+
|
|
101
|
+
project_path = get_project_path(slug)
|
|
102
|
+
if not project_path:
|
|
103
|
+
rprint("[red]No project path configured. Run 'bb init' first.[/red]")
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
|
|
106
|
+
token = get_token()
|
|
107
|
+
if not token:
|
|
108
|
+
rprint("[red]Not authenticated. Run 'bb login' first.[/red]")
|
|
109
|
+
raise typer.Exit(1)
|
|
110
|
+
|
|
111
|
+
daemon_id = _daemon_id()
|
|
112
|
+
api_url = get_api_url()
|
|
113
|
+
|
|
114
|
+
rprint(f"[bold green]Bumblebee Agent Daemon[/bold green]")
|
|
115
|
+
rprint(f" Project: [cyan]{slug}[/cyan]")
|
|
116
|
+
rprint(f" Path: [cyan]{project_path}[/cyan]")
|
|
117
|
+
rprint(f" API: [cyan]{api_url}[/cyan]")
|
|
118
|
+
rprint(f" Daemon: [cyan]{daemon_id}[/cyan]")
|
|
119
|
+
rprint(f" Poll: every {poll_interval}s")
|
|
120
|
+
rprint(f"\n[dim]Listening for web-initiated agent requests... (Ctrl+C to stop)[/dim]\n")
|
|
121
|
+
|
|
122
|
+
# Graceful shutdown
|
|
123
|
+
running = True
|
|
124
|
+
|
|
125
|
+
def _shutdown(sig, frame):
|
|
126
|
+
nonlocal running
|
|
127
|
+
rprint("\n[yellow]Shutting down daemon...[/yellow]")
|
|
128
|
+
running = False
|
|
129
|
+
|
|
130
|
+
signal.signal(signal.SIGINT, _shutdown)
|
|
131
|
+
signal.signal(signal.SIGTERM, _shutdown)
|
|
132
|
+
|
|
133
|
+
while running:
|
|
134
|
+
pending = _poll_pending_sessions(slug)
|
|
135
|
+
|
|
136
|
+
for session in pending:
|
|
137
|
+
claimed = _claim_session(session["id"], daemon_id)
|
|
138
|
+
if claimed:
|
|
139
|
+
rprint(f"[cyan]Claimed session {session['id']}[/cyan]")
|
|
140
|
+
try:
|
|
141
|
+
_run_session(slug, project_path, claimed)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
rprint(f"[red]Session {session['id']} failed: {e}[/red]")
|
|
144
|
+
try:
|
|
145
|
+
api_post(f"/api/agent-sessions/{session['id']}/complete", json={
|
|
146
|
+
"status": "failed", "error": str(e),
|
|
147
|
+
})
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
time.sleep(poll_interval)
|
|
152
|
+
|
|
153
|
+
rprint("[dim]Daemon stopped.[/dim]")
|
|
@@ -1,62 +1,83 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
import toml
|
|
4
|
-
import typer
|
|
5
|
-
from rich import print as rprint
|
|
6
|
-
|
|
7
|
-
from ..config import LOCAL_CONFIG_NAME, LOCAL_DIR_NAME, find_project_root
|
|
8
|
-
|
|
9
|
-
app = typer.Typer(help="Initialize project-local config")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@app.command()
|
|
13
|
-
def init(
|
|
14
|
-
project: str = typer.Option(None, "--project", "-p", help="Set current_project in local config"),
|
|
15
|
-
api_url: str = typer.Option(None, "--api-url", help="Set api_url in local config"),
|
|
16
|
-
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
|
|
17
|
-
):
|
|
18
|
-
"""Initialize a .bumblebee/ config directory in the project root.
|
|
19
|
-
|
|
20
|
-
Looks for the project root by walking up from cwd to find .git/.
|
|
21
|
-
Falls back to cwd if no .git/ is found.
|
|
22
|
-
"""
|
|
23
|
-
root = find_project_root()
|
|
24
|
-
if root is None:
|
|
25
|
-
root = Path.cwd().resolve()
|
|
26
|
-
|
|
27
|
-
bb_dir = root / LOCAL_DIR_NAME
|
|
28
|
-
config_file = bb_dir / LOCAL_CONFIG_NAME
|
|
29
|
-
gitignore_file = bb_dir / ".gitignore"
|
|
30
|
-
|
|
31
|
-
if config_file.exists() and not force:
|
|
32
|
-
rprint(f"[yellow].bumblebee/ already exists at {root}[/yellow]")
|
|
33
|
-
rprint("[dim]Use --force to overwrite.[/dim]")
|
|
34
|
-
raise typer.Exit(1)
|
|
35
|
-
|
|
36
|
-
# Create directory
|
|
37
|
-
bb_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
if project:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import toml
|
|
4
|
+
import typer
|
|
5
|
+
from rich import print as rprint
|
|
6
|
+
|
|
7
|
+
from ..config import LOCAL_CONFIG_NAME, LOCAL_DIR_NAME, find_project_root, set_project_path
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Initialize project-local config")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def init(
|
|
14
|
+
project: str = typer.Option(None, "--project", "-p", help="Set current_project in local config"),
|
|
15
|
+
api_url: str = typer.Option(None, "--api-url", help="Set api_url in local config"),
|
|
16
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
|
|
17
|
+
):
|
|
18
|
+
"""Initialize a .bumblebee/ config directory in the project root.
|
|
19
|
+
|
|
20
|
+
Looks for the project root by walking up from cwd to find .git/.
|
|
21
|
+
Falls back to cwd if no .git/ is found.
|
|
22
|
+
"""
|
|
23
|
+
root = find_project_root()
|
|
24
|
+
if root is None:
|
|
25
|
+
root = Path.cwd().resolve()
|
|
26
|
+
|
|
27
|
+
bb_dir = root / LOCAL_DIR_NAME
|
|
28
|
+
config_file = bb_dir / LOCAL_CONFIG_NAME
|
|
29
|
+
gitignore_file = bb_dir / ".gitignore"
|
|
30
|
+
|
|
31
|
+
if config_file.exists() and not force:
|
|
32
|
+
rprint(f"[yellow].bumblebee/ already exists at {root}[/yellow]")
|
|
33
|
+
rprint("[dim]Use --force to overwrite.[/dim]")
|
|
34
|
+
raise typer.Exit(1)
|
|
35
|
+
|
|
36
|
+
# Create directory
|
|
37
|
+
bb_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
# Interactive project selection when --project is not provided
|
|
40
|
+
selected_name = None
|
|
41
|
+
if not project:
|
|
42
|
+
try:
|
|
43
|
+
from .project import _select_project_interactive
|
|
44
|
+
|
|
45
|
+
selected = _select_project_interactive()
|
|
46
|
+
project = selected["slug"]
|
|
47
|
+
selected_name = selected["name"]
|
|
48
|
+
except (SystemExit, KeyboardInterrupt):
|
|
49
|
+
# User cancelled or no projects — continue without setting a project
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
# Build config
|
|
53
|
+
cfg: dict = {}
|
|
54
|
+
if project:
|
|
55
|
+
cfg["current_project"] = project
|
|
56
|
+
if api_url:
|
|
57
|
+
cfg["api_url"] = api_url
|
|
58
|
+
|
|
59
|
+
# Write config.toml
|
|
60
|
+
with open(config_file, "w") as f:
|
|
61
|
+
if cfg:
|
|
62
|
+
toml.dump(cfg, f)
|
|
63
|
+
else:
|
|
64
|
+
f.write("# Bumblebee project-local config\n# Settings here override ~/.bumblebee/config.toml\n")
|
|
65
|
+
|
|
66
|
+
# Write .gitignore to exclude local-only files
|
|
67
|
+
if not gitignore_file.exists() or force:
|
|
68
|
+
gitignore_file.write_text("*.local.toml\n")
|
|
69
|
+
|
|
70
|
+
# Auto-link the current directory as the project path
|
|
71
|
+
if project:
|
|
72
|
+
resolved = str(root.resolve())
|
|
73
|
+
set_project_path(project, resolved)
|
|
74
|
+
|
|
75
|
+
rprint(f"[green]Initialized .bumblebee/ at {root}[/green]")
|
|
76
|
+
if project:
|
|
77
|
+
display = f"{selected_name} ({project})" if selected_name else project
|
|
78
|
+
rprint(f" current_project = {display}")
|
|
79
|
+
rprint(f" linked path = {root.resolve()}")
|
|
80
|
+
if api_url:
|
|
81
|
+
rprint(f" api_url = {api_url}")
|
|
82
|
+
if not project:
|
|
83
|
+
rprint("[dim]Tip: Add project-specific settings to .bumblebee/config.toml[/dim]")
|