virtuai-cli 1.0.7__tar.gz → 1.0.8__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.
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/PKG-INFO +1 -1
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/pyproject.toml +1 -1
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/__init__.py +1 -1
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/look.py +4 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/tui.py +46 -7
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/widgets.py +5 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli.egg-info/PKG-INFO +1 -1
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/README.md +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/setup.cfg +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/browser.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/__init__.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/agents.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/ask.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/clipboard.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/command.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/devtools.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/highlight.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/history.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/journal.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/panels.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/review.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/sse.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/chat/steps.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/command_guard.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/config.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/executor.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/guard_setup.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/main.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/ollama_state.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/omlx_state.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/opencode.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/pull.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/runner.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli/security.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli.egg-info/SOURCES.txt +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli.egg-info/requires.txt +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/src/virtuai_cli.egg-info/top_level.txt +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_browser.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_command_guard.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_frame_concurrency.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_guard_setup.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_ollama_options.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_ollama_state.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_omlx.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_process_group_kill.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_proxy_env.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_pull_fallback.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_run_single_instance.py +0 -0
- {virtuai_cli-1.0.7 → virtuai_cli-1.0.8}/tests/test_session_isolation.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""VirtuAI local CLI."""
|
|
2
|
-
__version__ = "1.0.
|
|
2
|
+
__version__ = "1.0.8"
|
|
@@ -190,6 +190,10 @@ T = {
|
|
|
190
190
|
"guard_on": "🛡 comandos revisados por {model}" if _ES else "🛡 commands checked by {model}",
|
|
191
191
|
"guard_rules_only": "🛡 {why} — se pedirá permiso para lo que no sea lectura" if _ES
|
|
192
192
|
else "🛡 {why} — anything but reads will be asked",
|
|
193
|
+
"ran_runner": "💻 {name}",
|
|
194
|
+
"ran_workstation": "📦 {name}",
|
|
195
|
+
"ran_browser": "🌐 navegador" if _ES else "🌐 browser",
|
|
196
|
+
"guard_you": "tú lo autorizaste" if _ES else "you authorized it",
|
|
193
197
|
"guard_off": "⚠ comandos sin revisar" if _ES else "⚠ commands unchecked",
|
|
194
198
|
"guard_off_note": ("sin clasificador: lo que ejecute el agente es tu responsabilidad (virtuai guard setup)"
|
|
195
199
|
if _ES else "no classifier: what the agent runs is your responsibility (virtuai guard setup)"),
|
|
@@ -1923,11 +1923,12 @@ class ChatApp(App):
|
|
|
1923
1923
|
ok, what = await command_guard.available()
|
|
1924
1924
|
self._set_status(T["guard_on"].format(model=what) if ok else T["guard_rules_only"].format(why=what))
|
|
1925
1925
|
|
|
1926
|
-
async def _guard_decide(self, command: str, timeout: float) -> tuple[bool, str]:
|
|
1927
|
-
"""(run it?, what the guard thought). Asks you when
|
|
1926
|
+
async def _guard_decide(self, command: str, timeout: float) -> tuple[bool, str, bool]:
|
|
1927
|
+
"""(run it?, what the guard thought, were you asked?). Asks you when
|
|
1928
|
+
it is not clearly safe."""
|
|
1928
1929
|
verdict = await command_guard.classify(command, str(self.workdir))
|
|
1929
1930
|
if verdict.safe:
|
|
1930
|
-
return True, verdict.describe()
|
|
1931
|
+
return True, verdict.describe(), False
|
|
1931
1932
|
from virtuai_cli.chat.steps import CommandGuardPrompt
|
|
1932
1933
|
|
|
1933
1934
|
prompt = CommandGuardPrompt(command, verdict.describe())
|
|
@@ -1952,7 +1953,7 @@ class ChatApp(App):
|
|
|
1952
1953
|
inp.text = f"!{command}"
|
|
1953
1954
|
inp.move_cursor(inp.document.end)
|
|
1954
1955
|
self.query_one(ChatInput).focus()
|
|
1955
|
-
return allowed, verdict.describe()
|
|
1956
|
+
return allowed, verdict.describe(), True
|
|
1956
1957
|
|
|
1957
1958
|
@staticmethod
|
|
1958
1959
|
def _refusal(command: str, why: str) -> str:
|
|
@@ -1966,13 +1967,40 @@ class ChatApp(App):
|
|
|
1966
1967
|
"another way — wait, or continue without it."
|
|
1967
1968
|
)
|
|
1968
1969
|
|
|
1970
|
+
def _mark_card(self, command: str, note: str, asked: bool) -> None:
|
|
1971
|
+
"""Show on the command's step what the guard decided. The runner's
|
|
1972
|
+
frame ids are not the tool's, so the step is found by its command."""
|
|
1973
|
+
turn = self._current_turn
|
|
1974
|
+
if turn is None:
|
|
1975
|
+
return
|
|
1976
|
+
from virtuai_cli.chat.widgets import ToolCallCard
|
|
1977
|
+
|
|
1978
|
+
def command_of(card) -> str:
|
|
1979
|
+
text = card.input_str.strip()
|
|
1980
|
+
if text.startswith("{"):
|
|
1981
|
+
try:
|
|
1982
|
+
import json
|
|
1983
|
+
|
|
1984
|
+
return str(json.loads(text).get("command") or "").strip()
|
|
1985
|
+
except (ValueError, AttributeError):
|
|
1986
|
+
pass
|
|
1987
|
+
return text
|
|
1988
|
+
|
|
1989
|
+
for card in reversed(list(turn.query(ToolCallCard))):
|
|
1990
|
+
if not card.guard and command_of(card) == command.strip():
|
|
1991
|
+
card.guard, card.guard_asked = note, asked
|
|
1992
|
+
card._redraw()
|
|
1993
|
+
return
|
|
1994
|
+
|
|
1969
1995
|
async def _guard_command(self, command: str, frame: dict) -> Optional[str]:
|
|
1970
1996
|
"""The runner's COMMAND_GATE: None runs it, a string goes back instead."""
|
|
1971
1997
|
when = self._authorized.pop(command, None)
|
|
1972
1998
|
if when is not None and time.monotonic() - when < 300:
|
|
1999
|
+
self._mark_card(command, T["guard_you"], True)
|
|
1973
2000
|
return None # you already said yes to it, through the server's prompt
|
|
1974
2001
|
timeout = max(30.0, float(frame.get("timeout_secs") or 300) - 15)
|
|
1975
|
-
allowed, why = await self._guard_decide(command, timeout)
|
|
2002
|
+
allowed, why, asked = await self._guard_decide(command, timeout)
|
|
2003
|
+
self._mark_card(command, T["guard_you"] if asked and allowed else why, asked)
|
|
1976
2004
|
return None if allowed else self._refusal(command, why)
|
|
1977
2005
|
|
|
1978
2006
|
async def _guard_approval(self, event: dict) -> None:
|
|
@@ -1986,7 +2014,7 @@ class ChatApp(App):
|
|
|
1986
2014
|
allowed = True
|
|
1987
2015
|
else:
|
|
1988
2016
|
timeout = max(30.0, float(event.get("timeout_secs") or 300) - 15)
|
|
1989
|
-
allowed, why = await self._guard_decide(command, timeout)
|
|
2017
|
+
allowed, why, _ = await self._guard_decide(command, timeout)
|
|
1990
2018
|
if allowed:
|
|
1991
2019
|
self._authorized[command] = time.monotonic()
|
|
1992
2020
|
await self._post_approval(request_id, allowed, "" if allowed else self._why_not(why))
|
|
@@ -2314,13 +2342,24 @@ class ChatApp(App):
|
|
|
2314
2342
|
await turn.set_todos(step.get("items", []))
|
|
2315
2343
|
# usage, model_end, cancelled: nothing to show (web chat shows nothing either).
|
|
2316
2344
|
|
|
2345
|
+
@staticmethod
|
|
2346
|
+
def _where(loc) -> str:
|
|
2347
|
+
"""Where a step ran, as web chat says it — not the raw dict."""
|
|
2348
|
+
if isinstance(loc, dict):
|
|
2349
|
+
kind = loc.get("kind")
|
|
2350
|
+
if kind == "browser":
|
|
2351
|
+
return T["ran_browser"]
|
|
2352
|
+
key = "ran_workstation" if kind == "workstation" else "ran_runner"
|
|
2353
|
+
return T[key].format(name=loc.get("name") or kind or "")
|
|
2354
|
+
return str(loc or "")
|
|
2355
|
+
|
|
2317
2356
|
@staticmethod
|
|
2318
2357
|
def _decorate_card(card, event: dict) -> None:
|
|
2319
2358
|
"""What web chat shows on a step besides its name: where it ran, the
|
|
2320
2359
|
command's language, which knowledge base a search read."""
|
|
2321
2360
|
if card is None or not hasattr(card, "ran_in"):
|
|
2322
2361
|
return
|
|
2323
|
-
card.ran_in = event.get("ran_in")
|
|
2362
|
+
card.ran_in = ChatApp._where(event.get("ran_in"))
|
|
2324
2363
|
card.language = event.get("language") or ("bash" if event.get("display") == "execute" else "")
|
|
2325
2364
|
if event.get("display") == "kb_search" and event.get("kb_name"):
|
|
2326
2365
|
card.tool_name = f"📚 {event['kb_name']}"
|
|
@@ -797,6 +797,9 @@ class ToolCallCard(ExpandableCard):
|
|
|
797
797
|
self.ran_in: str = ""
|
|
798
798
|
self.elapsed: Optional[float] = None
|
|
799
799
|
self.language: str = ""
|
|
800
|
+
# What the command guard decided (command_guard.py), when it ran here.
|
|
801
|
+
self.guard: str = ""
|
|
802
|
+
self.guard_asked = False
|
|
800
803
|
self.add_class("-running")
|
|
801
804
|
self._redraw()
|
|
802
805
|
|
|
@@ -808,6 +811,8 @@ class ToolCallCard(ExpandableCard):
|
|
|
808
811
|
head.append(f" {self.elapsed:.1f}s", style="dim")
|
|
809
812
|
if self.ran_in:
|
|
810
813
|
head.append(f" · {self.ran_in}", style="dim")
|
|
814
|
+
if self.guard:
|
|
815
|
+
head.append(f" 🛡 {self.guard}", style="yellow" if self.guard_asked else "dim green")
|
|
811
816
|
|
|
812
817
|
cmd = self.input_str.strip()
|
|
813
818
|
cmd_lines = cmd.splitlines() if cmd else []
|
|
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
|