bevor-cli 0.1.0__py3-none-any.whl
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.
- bevor_cli/__init__.py +0 -0
- bevor_cli/annotations/__init__.py +0 -0
- bevor_cli/annotations/confirmations.py +16 -0
- bevor_cli/annotations/query.py +25 -0
- bevor_cli/apps/agent.py +10 -0
- bevor_cli/apps/analyses.py +47 -0
- bevor_cli/apps/chat.py +16 -0
- bevor_cli/apps/codes.py +18 -0
- bevor_cli/apps/findings.py +22 -0
- bevor_cli/apps/main.py +61 -0
- bevor_cli/apps/nodes.py +17 -0
- bevor_cli/apps/projects.py +14 -0
- bevor_cli/apps/sessions.py +13 -0
- bevor_cli/apps/workspaces.py +16 -0
- bevor_cli/client.py +157 -0
- bevor_cli/commands/__init__.py +0 -0
- bevor_cli/commands/agent/__init__.py +0 -0
- bevor_cli/commands/agent/materialze_cmd.py +65 -0
- bevor_cli/commands/agent/setup_cmd.py +18 -0
- bevor_cli/commands/analyses/__init__.py +0 -0
- bevor_cli/commands/analyses/active.py +42 -0
- bevor_cli/commands/analyses/chats/__init__.py +0 -0
- bevor_cli/commands/analyses/chats/get.py +39 -0
- bevor_cli/commands/analyses/chats/list.py +64 -0
- bevor_cli/commands/analyses/chats/messages.py +41 -0
- bevor_cli/commands/analyses/chats/new.py +44 -0
- bevor_cli/commands/analyses/chats/resume.py +43 -0
- bevor_cli/commands/analyses/chats/session.py +295 -0
- bevor_cli/commands/analyses/child.py +81 -0
- bevor_cli/commands/analyses/commit.py +49 -0
- bevor_cli/commands/analyses/create.py +105 -0
- bevor_cli/commands/analyses/delete.py +65 -0
- bevor_cli/commands/analyses/diff.py +87 -0
- bevor_cli/commands/analyses/entrypoints.py +56 -0
- bevor_cli/commands/analyses/findings.py +129 -0
- bevor_cli/commands/analyses/fork.py +47 -0
- bevor_cli/commands/analyses/list.py +85 -0
- bevor_cli/commands/analyses/merge.py +46 -0
- bevor_cli/commands/analyses/retry.py +77 -0
- bevor_cli/commands/analyses/run.py +87 -0
- bevor_cli/commands/analyses/select.py +85 -0
- bevor_cli/commands/analyses/set_head.py +45 -0
- bevor_cli/commands/analyses/transitions.py +90 -0
- bevor_cli/commands/analyses/view.py +94 -0
- bevor_cli/commands/auth/__init__.py +7 -0
- bevor_cli/commands/auth/login.py +135 -0
- bevor_cli/commands/auth/logout.py +23 -0
- bevor_cli/commands/auth/whoami.py +21 -0
- bevor_cli/commands/codes/__init__.py +0 -0
- bevor_cli/commands/codes/list.py +90 -0
- bevor_cli/commands/codes/push.py +76 -0
- bevor_cli/commands/codes/select.py +93 -0
- bevor_cli/commands/codes/view.py +80 -0
- bevor_cli/commands/context/__init__.py +92 -0
- bevor_cli/commands/findings/__init__.py +0 -0
- bevor_cli/commands/findings/acknowledge.py +28 -0
- bevor_cli/commands/findings/add.py +236 -0
- bevor_cli/commands/findings/bulk.py +110 -0
- bevor_cli/commands/findings/delete.py +28 -0
- bevor_cli/commands/findings/edit.py +109 -0
- bevor_cli/commands/findings/get.py +62 -0
- bevor_cli/commands/findings/interactive.py +329 -0
- bevor_cli/commands/findings/list.py +108 -0
- bevor_cli/commands/findings/revert.py +28 -0
- bevor_cli/commands/graph/__init__.py +0 -0
- bevor_cli/commands/graph/call_chain.py +48 -0
- bevor_cli/commands/graph/edges.py +59 -0
- bevor_cli/commands/graph/interactive.py +207 -0
- bevor_cli/commands/graph/renderer.py +135 -0
- bevor_cli/commands/link/__init__.py +52 -0
- bevor_cli/commands/link/helpers.py +26 -0
- bevor_cli/commands/nodes/__init__.py +0 -0
- bevor_cli/commands/nodes/get.py +52 -0
- bevor_cli/commands/nodes/list.py +75 -0
- bevor_cli/commands/nodes/source.py +40 -0
- bevor_cli/commands/projects/__init__.py +0 -0
- bevor_cli/commands/projects/create.py +54 -0
- bevor_cli/commands/projects/list.py +57 -0
- bevor_cli/commands/projects/select.py +75 -0
- bevor_cli/commands/projects/view.py +49 -0
- bevor_cli/commands/sessions/__init__.py +0 -0
- bevor_cli/commands/sessions/list.py +36 -0
- bevor_cli/commands/sessions/revoke.py +35 -0
- bevor_cli/commands/workspaces/__init__.py +0 -0
- bevor_cli/commands/workspaces/list.py +50 -0
- bevor_cli/commands/workspaces/pick.py +20 -0
- bevor_cli/commands/workspaces/select.py +68 -0
- bevor_cli/commands/workspaces/view.py +48 -0
- bevor_cli/configuration/creds.py +52 -0
- bevor_cli/configuration/settings.py +54 -0
- bevor_cli/configuration/state.py +113 -0
- bevor_cli/findings_yaml.py +291 -0
- bevor_cli/main.py +33 -0
- bevor_cli/output.py +69 -0
- bevor_cli/select.py +64 -0
- bevor_cli/settings.py +4 -0
- bevor_cli-0.1.0.dist-info/METADATA +14 -0
- bevor_cli-0.1.0.dist-info/RECORD +100 -0
- bevor_cli-0.1.0.dist-info/WHEEL +4 -0
- bevor_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""``bevor analysis chats list``."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from bevor_sdk.types.requests.chat import ChatsQueryParams
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from bevor_cli.annotations.confirmations import JSON_OUT
|
|
10
|
+
from bevor_cli.annotations.query import ORDER, ORDER_BY, PAGE, PAGE_SIZE
|
|
11
|
+
from bevor_cli.client import BevorInternalClient
|
|
12
|
+
from bevor_cli.configuration.creds import Credentials
|
|
13
|
+
from bevor_cli.configuration.settings import Settings
|
|
14
|
+
from bevor_cli.configuration.state import State
|
|
15
|
+
from bevor_cli.output import console
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_chats(
|
|
19
|
+
page: PAGE,
|
|
20
|
+
page_size: PAGE_SIZE,
|
|
21
|
+
order: ORDER,
|
|
22
|
+
order_by: ORDER_BY,
|
|
23
|
+
project: Annotated[Optional[str], typer.Option("--project", help="project to list chats for")],
|
|
24
|
+
json_out: JSON_OUT = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""List chats. Uses the currently configured project, or --project"""
|
|
27
|
+
creds = Credentials.load()
|
|
28
|
+
creds.require_token()
|
|
29
|
+
|
|
30
|
+
state = State.load()
|
|
31
|
+
settings = Settings.load()
|
|
32
|
+
workspace_id = state.validate_workspace()
|
|
33
|
+
client = BevorInternalClient(
|
|
34
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project(priority=project)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
query_params = ChatsQueryParams(
|
|
38
|
+
page=page,
|
|
39
|
+
page_size=page_size or settings.default_page_size,
|
|
40
|
+
order=order,
|
|
41
|
+
order_by=order_by,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
found = client.chats.list(options=query_params)
|
|
45
|
+
|
|
46
|
+
if json_out:
|
|
47
|
+
console.print_json(found.model_dump_json(indent=2))
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
if not found.results:
|
|
51
|
+
console.print("No chats found.")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
table = Table("ID", "Title", "Code", "Version", "Created")
|
|
55
|
+
for c in found.results:
|
|
56
|
+
table.add_row(
|
|
57
|
+
str(c.id),
|
|
58
|
+
c.title or "",
|
|
59
|
+
str(c.code_id),
|
|
60
|
+
str(c.version_id or ""),
|
|
61
|
+
str(c.created_at.date()),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
console.print(table)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""``bevor analyses chats messages``."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from bevor_sdk.types import ID
|
|
7
|
+
|
|
8
|
+
from bevor_cli.annotations.confirmations import JSON_OUT
|
|
9
|
+
from bevor_cli.client import BevorInternalClient
|
|
10
|
+
from bevor_cli.configuration.creds import Credentials
|
|
11
|
+
from bevor_cli.configuration.state import State
|
|
12
|
+
from bevor_cli.output import console
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def chat_messages(
|
|
16
|
+
chat_id: str = typer.Argument(..., help="Chat ID."),
|
|
17
|
+
json_out: JSON_OUT = False,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Get chat messages for a chat."""
|
|
20
|
+
creds = Credentials.load()
|
|
21
|
+
creds.require_token()
|
|
22
|
+
|
|
23
|
+
state = State.load()
|
|
24
|
+
workspace_id = state.validate_workspace()
|
|
25
|
+
|
|
26
|
+
client = BevorInternalClient(
|
|
27
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
messages = client.chats.messages(ID(chat_id))
|
|
31
|
+
|
|
32
|
+
if json_out:
|
|
33
|
+
console.print_json(
|
|
34
|
+
json.dumps([m.model_dump(mode="json") for m in messages.results], indent=2)
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
for m in messages.results:
|
|
39
|
+
role = m.chat_role or ""
|
|
40
|
+
text = m.message or ""
|
|
41
|
+
console.print(f"[{str(role):>9}] {text[:120]}")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from bevor_cli.client import BevorInternalClient
|
|
6
|
+
from bevor_cli.commands.analyses.chats.session import _open_streaming_session
|
|
7
|
+
from bevor_cli.configuration.creds import Credentials
|
|
8
|
+
from bevor_cli.configuration.state import State
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def chat_new(
|
|
12
|
+
mode: Literal["analysis", "code"] = typer.Argument(
|
|
13
|
+
..., help="whether to create a chat with analysis or code"
|
|
14
|
+
),
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Create a chat thread and open the streaming terminal session. The mode determines if you want
|
|
17
|
+
to chat with just code, or with analysis + code. Either use your current configuration."""
|
|
18
|
+
|
|
19
|
+
creds = Credentials.load()
|
|
20
|
+
creds.require_token()
|
|
21
|
+
|
|
22
|
+
state = State.load()
|
|
23
|
+
workspace_id = state.validate_workspace()
|
|
24
|
+
|
|
25
|
+
client = BevorInternalClient(
|
|
26
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if mode == "analysis":
|
|
30
|
+
analysis_id = state.validate_analysis()
|
|
31
|
+
analysis = client.analyses.get(analysis_id)
|
|
32
|
+
created = client.chats.create_for_analysis(version_id=analysis.version.id)
|
|
33
|
+
_open_streaming_session(
|
|
34
|
+
client,
|
|
35
|
+
analysis_id=analysis_id,
|
|
36
|
+
version_id=analysis.version.id,
|
|
37
|
+
thread_id=created.id,
|
|
38
|
+
)
|
|
39
|
+
elif mode == "code":
|
|
40
|
+
code_id = state.validate_code()
|
|
41
|
+
created = client.chats.create_for_code(code_id=code_id)
|
|
42
|
+
_open_streaming_session(client, analysis_id=None, thread_id=created.id)
|
|
43
|
+
else:
|
|
44
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""``bevor chats`` — interactive streaming chat for the active security analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from bevor_cli import output
|
|
8
|
+
from bevor_cli.client import BevorInternalClient
|
|
9
|
+
from bevor_cli.commands.analyses.chats.session import (
|
|
10
|
+
_ensure_thread_matches_analysis,
|
|
11
|
+
_open_streaming_session,
|
|
12
|
+
)
|
|
13
|
+
from bevor_cli.configuration.creds import Credentials
|
|
14
|
+
from bevor_cli.configuration.state import State
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def chat_resume(
|
|
18
|
+
chat_id: str = typer.Argument(
|
|
19
|
+
...,
|
|
20
|
+
metavar="CHAT_ID",
|
|
21
|
+
help="Thread id (`bevor chats list --json`, field id).",
|
|
22
|
+
),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Open the streaming session for an existing thread."""
|
|
25
|
+
creds = Credentials.load()
|
|
26
|
+
creds.require_token()
|
|
27
|
+
|
|
28
|
+
state = State.load()
|
|
29
|
+
workspace_id = state.validate_workspace()
|
|
30
|
+
|
|
31
|
+
if not state.analysis_id:
|
|
32
|
+
output.error(
|
|
33
|
+
"No analysis is configured.",
|
|
34
|
+
hint="Run `bevor analyses select` or `bevor analyses create`.",
|
|
35
|
+
)
|
|
36
|
+
raise typer.Exit(1)
|
|
37
|
+
|
|
38
|
+
client = BevorInternalClient(
|
|
39
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
40
|
+
)
|
|
41
|
+
tid = chat_id.strip()
|
|
42
|
+
_ensure_thread_matches_analysis(client, tid, state.analysis_id)
|
|
43
|
+
_open_streaming_session(client, analysis_id=state.analysis_id, thread_id=tid)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""``bevor chats`` — interactive streaming chat for the active security analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import itertools
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import questionary
|
|
12
|
+
import typer
|
|
13
|
+
from bevor_sdk._sync.resources.chat.chats import BoundChatResources
|
|
14
|
+
from bevor_sdk.types import ID
|
|
15
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
16
|
+
from rich.live import Live
|
|
17
|
+
from rich.markdown import CodeBlock, Markdown
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
from bevor_cli import output
|
|
22
|
+
from bevor_cli.client import BevorInternalClient
|
|
23
|
+
|
|
24
|
+
_CHAT_STYLE = questionary.Style(
|
|
25
|
+
[
|
|
26
|
+
("highlighted", "bold"),
|
|
27
|
+
("selected", "bold fg:ansibrightyellow"),
|
|
28
|
+
("instruction", "fg:ansibrightblack italic"),
|
|
29
|
+
("pointer", "bold fg:ansibrightyellow"),
|
|
30
|
+
]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_STREAM_HTTP_TIMEOUT = httpx.Timeout(600.0, connect=60.0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _recv_spinner(stop: threading.Event) -> None:
|
|
37
|
+
frames = itertools.cycle("⠋⠙⠹⠸⠼⠴⠦⠇⠏")
|
|
38
|
+
while not stop.is_set():
|
|
39
|
+
sys.stdout.write(f"\r\033[2K{next(frames)} Receiving…")
|
|
40
|
+
sys.stdout.flush()
|
|
41
|
+
stop.wait(0.09)
|
|
42
|
+
sys.stdout.write("\r\033[2K")
|
|
43
|
+
sys.stdout.flush()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _stream_fence_buffer(content: str) -> str:
|
|
47
|
+
"""Synthetic closing fence for render only (same rule as web ``setBuffer('\\n```')`` when odd)."""
|
|
48
|
+
if content.count("```") % 2 != 0:
|
|
49
|
+
return "\n```"
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _stream_markdown_markup(streamed_content: str) -> str:
|
|
54
|
+
"""What the UI renders: ``streamedContent + buffer``."""
|
|
55
|
+
return streamed_content + _stream_fence_buffer(streamed_content)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _PlainCodeBlock(CodeBlock):
|
|
59
|
+
"""Fenced bodies during SSE: monospace wrap only — Rich's default uses Pygments and stalls."""
|
|
60
|
+
|
|
61
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
62
|
+
code = str(self.text).rstrip()
|
|
63
|
+
yield Text(code, style=self.style_name, overflow="fold")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _StreamingLiveMarkdown(Markdown):
|
|
67
|
+
elements = {**Markdown.elements, "code_block": _PlainCodeBlock}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _stream_markdown_renderable(markup: str) -> Markdown:
|
|
71
|
+
return _StreamingLiveMarkdown(markup)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _run_stream_turn(
|
|
75
|
+
bound: BoundChatResources,
|
|
76
|
+
*,
|
|
77
|
+
http: httpx.Client,
|
|
78
|
+
message: str,
|
|
79
|
+
approval_id: str | None,
|
|
80
|
+
is_approved: bool | None,
|
|
81
|
+
) -> tuple[List[str], str | None, str | None]:
|
|
82
|
+
"""Consume one POST /stream response.
|
|
83
|
+
|
|
84
|
+
Returns ``([], approval_tool_id, approval_prompt)`` — reference events are ignored in the CLI.
|
|
85
|
+
|
|
86
|
+
Text events mirror the web client: ``streamedContent`` is the raw payload; for Markdown we append a
|
|
87
|
+
closing-fence buffer when ``` count is odd. Live updates use markdown with plain fenced bodies so
|
|
88
|
+
Pygments is not invoked on every chunk (that was freezing the stream once a fence opened).
|
|
89
|
+
"""
|
|
90
|
+
kw: Dict[str, Any] = {
|
|
91
|
+
"message": message,
|
|
92
|
+
"client": http,
|
|
93
|
+
}
|
|
94
|
+
if approval_id is not None:
|
|
95
|
+
kw["approval_id"] = approval_id
|
|
96
|
+
kw["is_approved"] = bool(is_approved)
|
|
97
|
+
|
|
98
|
+
references: List[str] = []
|
|
99
|
+
pending_tool: str | None = None
|
|
100
|
+
pending_prompt: str | None = None
|
|
101
|
+
|
|
102
|
+
stream_live: Live | None = None
|
|
103
|
+
live_text_active = False
|
|
104
|
+
final_stream_text: str | None = None
|
|
105
|
+
|
|
106
|
+
spinner_stop = threading.Event()
|
|
107
|
+
spinner_thread: threading.Thread | None = None
|
|
108
|
+
|
|
109
|
+
def stop_recv_spinner() -> None:
|
|
110
|
+
nonlocal spinner_thread
|
|
111
|
+
spinner_stop.set()
|
|
112
|
+
if spinner_thread is not None:
|
|
113
|
+
spinner_thread.join(timeout=1.0)
|
|
114
|
+
spinner_thread = None
|
|
115
|
+
|
|
116
|
+
def stop_stream_live() -> None:
|
|
117
|
+
nonlocal stream_live
|
|
118
|
+
if stream_live is not None:
|
|
119
|
+
stream_live.stop()
|
|
120
|
+
stream_live = None
|
|
121
|
+
|
|
122
|
+
def flush_streamed_markdown(*, quiet_plain_fallback: bool) -> None:
|
|
123
|
+
nonlocal live_text_active, final_stream_text
|
|
124
|
+
stop_stream_live()
|
|
125
|
+
if quiet_plain_fallback and final_stream_text is not None:
|
|
126
|
+
output.console.print(
|
|
127
|
+
Markdown(_stream_markdown_markup(final_stream_text)),
|
|
128
|
+
end="\n",
|
|
129
|
+
)
|
|
130
|
+
live_text_active = False
|
|
131
|
+
final_stream_text = None
|
|
132
|
+
|
|
133
|
+
output.console.print("\n[bold cyan]Assistant[/bold cyan]")
|
|
134
|
+
sys.stdout.flush()
|
|
135
|
+
|
|
136
|
+
if not output.is_quiet():
|
|
137
|
+
spinner_thread = threading.Thread(
|
|
138
|
+
target=_recv_spinner,
|
|
139
|
+
args=(spinner_stop,),
|
|
140
|
+
daemon=True,
|
|
141
|
+
)
|
|
142
|
+
assert spinner_thread
|
|
143
|
+
spinner_thread.start()
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
for obj in bound.stream(**kw):
|
|
147
|
+
if not obj:
|
|
148
|
+
continue
|
|
149
|
+
stop_recv_spinner()
|
|
150
|
+
|
|
151
|
+
if obj.event_type == "text":
|
|
152
|
+
raw = str(obj.content or "")
|
|
153
|
+
final_stream_text = raw
|
|
154
|
+
markup = _stream_markdown_markup(raw)
|
|
155
|
+
if output.is_quiet():
|
|
156
|
+
live_text_active = True
|
|
157
|
+
continue
|
|
158
|
+
md = _stream_markdown_renderable(markup)
|
|
159
|
+
if stream_live is None:
|
|
160
|
+
stream_live = Live(
|
|
161
|
+
md,
|
|
162
|
+
console=output.console,
|
|
163
|
+
refresh_per_second=15,
|
|
164
|
+
transient=False,
|
|
165
|
+
vertical_overflow="visible",
|
|
166
|
+
redirect_stdout=False,
|
|
167
|
+
redirect_stderr=False,
|
|
168
|
+
)
|
|
169
|
+
assert stream_live
|
|
170
|
+
stream_live.start()
|
|
171
|
+
else:
|
|
172
|
+
stream_live.update(md)
|
|
173
|
+
live_text_active = True
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
if live_text_active:
|
|
177
|
+
flush_streamed_markdown(quiet_plain_fallback=output.is_quiet())
|
|
178
|
+
|
|
179
|
+
if obj.event_type == "thinking":
|
|
180
|
+
output.console.print(f"[dim italic]{obj.content or 'thinking…'}[/dim italic]")
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
if obj.event_type == "tool-call":
|
|
184
|
+
output.console.print(f"[dim]tool[/dim] [yellow]{obj.content}[/yellow]")
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
if obj.event_type == "approval":
|
|
188
|
+
pending_tool = obj.id
|
|
189
|
+
pending_prompt = obj.content or "Approve this tool call?"
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
if obj.event_type == "failure":
|
|
193
|
+
output.console.print(f"[bold red]{obj.content or 'Request failed'}[/bold red]")
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
if obj.event_type == "reference":
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
if live_text_active:
|
|
200
|
+
flush_streamed_markdown(quiet_plain_fallback=output.is_quiet())
|
|
201
|
+
finally:
|
|
202
|
+
stop_recv_spinner()
|
|
203
|
+
|
|
204
|
+
return references, pending_tool, pending_prompt
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _session_banner(*, analysis_id: str | None, version_id: str | None, thread_id: str) -> None:
|
|
208
|
+
context = ""
|
|
209
|
+
if analysis_id:
|
|
210
|
+
context += f"[bold]Analysis[/bold] {analysis_id}\n"
|
|
211
|
+
if version_id:
|
|
212
|
+
context += f"[bold]Version[/bold] {version_id}\n"
|
|
213
|
+
output.console.print(
|
|
214
|
+
Panel.fit(
|
|
215
|
+
f"{context}[bold]Thread[/bold] {thread_id}\n\n"
|
|
216
|
+
"[dim]/quit · /exit · Ctrl+D · requires synced code (`bevor codes push`)[/dim]",
|
|
217
|
+
title="Bevor Chat",
|
|
218
|
+
border_style="blue",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _ensure_thread_matches_analysis(security: BevorInternalClient, thread_id: str, aid: str) -> None:
|
|
224
|
+
chat = security.chats.get(ID(thread_id))
|
|
225
|
+
if chat.version_id is not None and str(chat.version_id) != aid:
|
|
226
|
+
output.error(
|
|
227
|
+
f"Chat {thread_id} belongs to another analysis ({chat.version_id}).",
|
|
228
|
+
hint="Pick a thread from `bevor chats list` or run `bevor analyses select`.",
|
|
229
|
+
)
|
|
230
|
+
raise typer.Exit(1)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _open_streaming_session(
|
|
234
|
+
security: BevorInternalClient,
|
|
235
|
+
*,
|
|
236
|
+
analysis_id: str | None,
|
|
237
|
+
version_id: str | None = None,
|
|
238
|
+
thread_id: str,
|
|
239
|
+
) -> None:
|
|
240
|
+
_session_banner(analysis_id=analysis_id, version_id=version_id, thread_id=thread_id)
|
|
241
|
+
try:
|
|
242
|
+
_interactive_loop(security, thread_id=thread_id)
|
|
243
|
+
except KeyboardInterrupt:
|
|
244
|
+
output.console.print("\n[dim]Interrupted.[/dim]")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _interactive_loop(client: BevorInternalClient, *, thread_id: str) -> None:
|
|
248
|
+
bound = client.chats(ID(thread_id))
|
|
249
|
+
|
|
250
|
+
with httpx.Client(timeout=_STREAM_HTTP_TIMEOUT) as http:
|
|
251
|
+
while True:
|
|
252
|
+
user_line = questionary.text(
|
|
253
|
+
"You ›",
|
|
254
|
+
qmark="",
|
|
255
|
+
style=_CHAT_STYLE,
|
|
256
|
+
).ask()
|
|
257
|
+
|
|
258
|
+
if user_line is None:
|
|
259
|
+
output.console.print("[dim]Goodbye.[/dim]")
|
|
260
|
+
break
|
|
261
|
+
|
|
262
|
+
text = user_line.strip()
|
|
263
|
+
if not text:
|
|
264
|
+
continue
|
|
265
|
+
low = text.lower()
|
|
266
|
+
if low in ("/quit", "/exit", ":q", "exit", "quit"):
|
|
267
|
+
output.console.print("[dim]Goodbye.[/dim]")
|
|
268
|
+
break
|
|
269
|
+
|
|
270
|
+
approval_id: str | None = None
|
|
271
|
+
is_approved: bool | None = None
|
|
272
|
+
|
|
273
|
+
while True:
|
|
274
|
+
refs, pending_tool, pending_prompt = _run_stream_turn(
|
|
275
|
+
bound,
|
|
276
|
+
http=http,
|
|
277
|
+
message=text,
|
|
278
|
+
approval_id=approval_id,
|
|
279
|
+
is_approved=is_approved,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if pending_tool:
|
|
283
|
+
choice = questionary.confirm(
|
|
284
|
+
pending_prompt or "Approve tool execution?",
|
|
285
|
+
default=False,
|
|
286
|
+
style=_CHAT_STYLE,
|
|
287
|
+
).ask()
|
|
288
|
+
approved = bool(choice) if choice is not None else False
|
|
289
|
+
approval_id = pending_tool
|
|
290
|
+
is_approved = approved
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
for row in refs:
|
|
294
|
+
output.console.print(row)
|
|
295
|
+
break
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""``bevor analysis child`` — create a child version on new code."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from bevor_sdk.types import ID
|
|
7
|
+
from bevor_sdk.types.requests.security import CreateAnalysisChildBodyDict
|
|
8
|
+
from bevor_sdk.types.responses.security import AnalysisCollisionResult, AnalysisCreatedResult
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from bevor_cli import output
|
|
12
|
+
from bevor_cli.client import BevorInternalClient
|
|
13
|
+
from bevor_cli.configuration.creds import Credentials
|
|
14
|
+
from bevor_cli.configuration.state import State
|
|
15
|
+
from bevor_cli.output import console
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_child_analysis(
|
|
19
|
+
version: Optional[str] = typer.Option(
|
|
20
|
+
None,
|
|
21
|
+
"--version",
|
|
22
|
+
help="Parent version id (default: configured analysis head).",
|
|
23
|
+
),
|
|
24
|
+
collision: Optional[Literal["surface", "force", "merge"]] = typer.Option(
|
|
25
|
+
"surface",
|
|
26
|
+
"--collision",
|
|
27
|
+
help="What Bevor should do if analysis already exists for the given code",
|
|
28
|
+
),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Create a child analysis version using the configured code. If there is no currently configured
|
|
31
|
+
analysis (`bevor context` or `bevor analyses view`), you can pass a --version, or you can select an analysis
|
|
32
|
+
using `bevor analyses select`. Bevor will check whether an analysis
|
|
33
|
+
exists for the given code version or not, and early return if so. Use the --collision argument to bypass this,
|
|
34
|
+
but it's advised not to."""
|
|
35
|
+
|
|
36
|
+
creds = Credentials.load()
|
|
37
|
+
creds.require_token()
|
|
38
|
+
|
|
39
|
+
state = State.load()
|
|
40
|
+
workspace_id = state.validate_workspace()
|
|
41
|
+
code_id = state.validate_code()
|
|
42
|
+
|
|
43
|
+
client = BevorInternalClient(
|
|
44
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
body = CreateAnalysisChildBodyDict(code_id=code_id, on_collision=collision)
|
|
48
|
+
if version:
|
|
49
|
+
v_ = client.analysis_versions.get(ID(version))
|
|
50
|
+
analysis_id = v_.analysis_meta.id
|
|
51
|
+
version_id = v_.id
|
|
52
|
+
body["version_id"] = version_id
|
|
53
|
+
else:
|
|
54
|
+
analysis_id = state.validate_analysis()
|
|
55
|
+
|
|
56
|
+
created = client.analyses.create_child(analysis_id, body)
|
|
57
|
+
|
|
58
|
+
if isinstance(created, AnalysisCreatedResult):
|
|
59
|
+
state.set_analysis(created.analysis)
|
|
60
|
+
state.save()
|
|
61
|
+
|
|
62
|
+
output.success(
|
|
63
|
+
f"Child analysis version created: {created.analysis.version.id} "
|
|
64
|
+
f"(analysis {created.analysis.id})."
|
|
65
|
+
)
|
|
66
|
+
elif isinstance(created, AnalysisCollisionResult):
|
|
67
|
+
output.warning(
|
|
68
|
+
f"{len(created.candidates)} analysis versions already exist for this code version. They're listed out with the proposed actions. "
|
|
69
|
+
"if you want to create the analysis version anyways, use the --collision=force flag."
|
|
70
|
+
)
|
|
71
|
+
table = Table("ID", "Name", "Created", "User", "Action")
|
|
72
|
+
for candidate in created.candidates:
|
|
73
|
+
args = [
|
|
74
|
+
candidate.id,
|
|
75
|
+
candidate.analysis_meta.name,
|
|
76
|
+
str(candidate.created_at.date()),
|
|
77
|
+
candidate.analysis_meta.user.username,
|
|
78
|
+
candidate.on_collision,
|
|
79
|
+
]
|
|
80
|
+
table.add_row(*args)
|
|
81
|
+
console.print(table)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""``bevor analysis commit`` — commit staged finding changes."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from bevor_sdk.types import ID
|
|
7
|
+
from bevor_sdk.types.requests.security import VersionBodyDict
|
|
8
|
+
|
|
9
|
+
from bevor_cli import output
|
|
10
|
+
from bevor_cli.client import BevorInternalClient
|
|
11
|
+
from bevor_cli.configuration.creds import Credentials
|
|
12
|
+
from bevor_cli.configuration.state import State
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def commit_analysis(
|
|
16
|
+
version: Optional[str] = typer.Option(
|
|
17
|
+
None,
|
|
18
|
+
"--version",
|
|
19
|
+
help="Version id to commit (default: configured analysis head).",
|
|
20
|
+
),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Commit staged findings for an analysis. This will update the HEAD of the configured analysis, or of the
|
|
23
|
+
analysis associated to the --version."""
|
|
24
|
+
creds = Credentials.load()
|
|
25
|
+
creds.require_token()
|
|
26
|
+
|
|
27
|
+
state = State.load()
|
|
28
|
+
workspace_id = state.validate_workspace()
|
|
29
|
+
analysis_id = state.validate_analysis()
|
|
30
|
+
|
|
31
|
+
client = BevorInternalClient(
|
|
32
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
body = VersionBodyDict()
|
|
36
|
+
if version:
|
|
37
|
+
v_ = client.analysis_versions.get(ID(version))
|
|
38
|
+
analysis_id = v_.analysis_meta.id
|
|
39
|
+
version_id = v_.id
|
|
40
|
+
body["version_id"] = version_id
|
|
41
|
+
else:
|
|
42
|
+
analysis_id = state.validate_analysis()
|
|
43
|
+
|
|
44
|
+
result = client.analyses.commit(analysis_id, body)
|
|
45
|
+
|
|
46
|
+
state.set_analysis(result)
|
|
47
|
+
state.save()
|
|
48
|
+
|
|
49
|
+
output.success(f"Committed changes → new head {result.version.id} (analysis {result.id}).")
|