mvgeos-cli 0.6.14__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.
- mvgeos_cli-0.6.14/PKG-INFO +15 -0
- mvgeos_cli-0.6.14/pyproject.toml +42 -0
- mvgeos_cli-0.6.14/pyproject.toml.orig +36 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/__init__.py +3 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/__main__.py +6 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/agent_factory.py +145 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/approval_binding.py +145 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/approval_presenter.py +584 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/approval_types.py +72 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/__init__.py +1 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/build.py +182 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/config.py +118 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/dispatcher.py +183 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/info.py +269 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/mvge.py +96 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/repl.py +774 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/rune.py +56 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/setup.py +453 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/tome.py +429 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/commands/tui.py +540 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/console.py +74 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/dynamic_commands.py +312 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/formatting.py +323 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/main.py +572 -0
- mvgeos_cli-0.6.14/src/mvgeos_cli/py.typed +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mvgeos-cli
|
|
3
|
+
Version: 0.6.14
|
|
4
|
+
Summary: CLI entry point for MvgeOS
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Dist: mvgeos-agent>=0.6.14
|
|
7
|
+
Requires-Dist: mvgeos-core>=0.6.14
|
|
8
|
+
Requires-Dist: mvgeos-provider>=0.6.14
|
|
9
|
+
Requires-Dist: mvgeos-tome>=0.6.14
|
|
10
|
+
Requires-Dist: mvgeos-runes>=0.6.14
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Requires-Dist: typer>=0.27.1
|
|
13
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
14
|
+
Requires-Dist: click>=8.0
|
|
15
|
+
Requires-Python: >=3.13
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mvgeos-cli"
|
|
3
|
+
version = "0.6.14"
|
|
4
|
+
description = "CLI entry point for MvgeOS"
|
|
5
|
+
requires-python = ">=3.13"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"mvgeos-agent>=0.6.14",
|
|
9
|
+
"mvgeos-core>=0.6.14",
|
|
10
|
+
"mvgeos-provider>=0.6.14",
|
|
11
|
+
"mvgeos-tome>=0.6.14",
|
|
12
|
+
"mvgeos-runes>=0.6.14",
|
|
13
|
+
"rich>=13.0",
|
|
14
|
+
"typer>=0.27.1",
|
|
15
|
+
"prompt-toolkit>=3.0",
|
|
16
|
+
"click>=8.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
mvgeos = "mvgeos_cli.main:main"
|
|
21
|
+
|
|
22
|
+
[tool.uv.sources.mvgeos-agent]
|
|
23
|
+
workspace = true
|
|
24
|
+
|
|
25
|
+
[tool.uv.sources.mvgeos-core]
|
|
26
|
+
workspace = true
|
|
27
|
+
|
|
28
|
+
[tool.uv.sources.mvgeos-provider]
|
|
29
|
+
workspace = true
|
|
30
|
+
|
|
31
|
+
[tool.uv.sources.mvgeos-tome]
|
|
32
|
+
workspace = true
|
|
33
|
+
|
|
34
|
+
[tool.uv.sources.mvgeos-runes]
|
|
35
|
+
workspace = true
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
extend = "../pyproject.toml"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["uv_build>=0.11.2,<0.13"]
|
|
42
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mvgeos-cli"
|
|
3
|
+
version = "0.6.14"
|
|
4
|
+
description = "CLI entry point for MvgeOS"
|
|
5
|
+
requires-python = ">=3.13"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
dependencies = [
|
|
8
|
+
# Explicit floors: uv_build does not substitute the workspace version, so
|
|
9
|
+
# bare names ship as unbounded Requires-Dist. Bumped in lockstep by ci.yml.
|
|
10
|
+
"mvgeos-agent>=0.6.14",
|
|
11
|
+
"mvgeos-core>=0.6.14",
|
|
12
|
+
"mvgeos-provider>=0.6.14",
|
|
13
|
+
"mvgeos-tome>=0.6.14",
|
|
14
|
+
"mvgeos-runes>=0.6.14",
|
|
15
|
+
"rich>=13.0",
|
|
16
|
+
"typer>=0.27.1",
|
|
17
|
+
"prompt-toolkit>=3.0",
|
|
18
|
+
"click>=8.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
mvgeos = "mvgeos_cli.main:main"
|
|
23
|
+
|
|
24
|
+
[tool.uv.sources]
|
|
25
|
+
mvgeos-agent = { workspace = true }
|
|
26
|
+
mvgeos-core = { workspace = true }
|
|
27
|
+
mvgeos-provider = { workspace = true }
|
|
28
|
+
mvgeos-tome = { workspace = true }
|
|
29
|
+
mvgeos-runes = { workspace = true }
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.11.2,<0.13"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
extend = "../pyproject.toml"
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mvgeos_agent import Mvge
|
|
8
|
+
from mvgeos_agent.environment import MvgeEnvironment
|
|
9
|
+
from mvgeos_agent.protocol import AgentFactory, MvgeAgent
|
|
10
|
+
from mvgeos_core.constants import DEFAULT_AGENT_NAME
|
|
11
|
+
from mvgeos_core.layers import agent_dir
|
|
12
|
+
|
|
13
|
+
from mvgeos_cli.dynamic_commands import cli_project_dir
|
|
14
|
+
|
|
15
|
+
_global_default_factory: AgentFactory | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_api_key(api_key: str, model_id: str | None = None) -> None:
|
|
19
|
+
"""Validate API key format. Raises ValueError if invalid."""
|
|
20
|
+
if model_id and model_id.startswith("ollama/"):
|
|
21
|
+
return
|
|
22
|
+
if api_key.startswith("sk-or-") or api_key.startswith("AIza"):
|
|
23
|
+
return
|
|
24
|
+
if model_id and model_id.startswith("google/") and api_key.strip():
|
|
25
|
+
return
|
|
26
|
+
if not api_key:
|
|
27
|
+
raise ValueError("API key is required.")
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"Invalid API key format. OpenRouter keys start with 'sk-or-', "
|
|
30
|
+
"and Google keys start with 'AIza'."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_agent_factory(
|
|
35
|
+
*,
|
|
36
|
+
api_key: str = "",
|
|
37
|
+
name: str = DEFAULT_AGENT_NAME,
|
|
38
|
+
custom_system_prompt: str = "",
|
|
39
|
+
extension_dir: str | None = None,
|
|
40
|
+
tome_dir: Path | None = None,
|
|
41
|
+
tome_resume: str | None = None,
|
|
42
|
+
provider_name: str | None = None,
|
|
43
|
+
environment: MvgeEnvironment | None = None,
|
|
44
|
+
strict_resume: bool = False,
|
|
45
|
+
force_fork_resume: bool = False,
|
|
46
|
+
**kwargs: Any,
|
|
47
|
+
) -> MvgeAgent:
|
|
48
|
+
"""Default agent factory using Mvge."""
|
|
49
|
+
resolved_name = name
|
|
50
|
+
if resolved_name == DEFAULT_AGENT_NAME and agent_dir("coding_mvge").is_dir():
|
|
51
|
+
resolved_name = "coding_mvge"
|
|
52
|
+
|
|
53
|
+
return Mvge(
|
|
54
|
+
api_key=api_key or None,
|
|
55
|
+
name=resolved_name,
|
|
56
|
+
spells=kwargs.get("spells"),
|
|
57
|
+
custom_system_prompt=custom_system_prompt,
|
|
58
|
+
extension_dir=extension_dir,
|
|
59
|
+
tome_dir=tome_dir,
|
|
60
|
+
tome_resume=tome_resume,
|
|
61
|
+
provider_name=provider_name,
|
|
62
|
+
environment=environment,
|
|
63
|
+
strict_resume=strict_resume,
|
|
64
|
+
force_fork_resume=force_fork_resume,
|
|
65
|
+
provider_registry=kwargs.get("provider_registry"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_default_agent_factory() -> AgentFactory:
|
|
70
|
+
"""Return the currently registered default agent factory."""
|
|
71
|
+
return _global_default_factory or default_agent_factory
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def set_default_agent_factory(factory: AgentFactory | None) -> None:
|
|
75
|
+
"""Set or reset the global default agent factory."""
|
|
76
|
+
global _global_default_factory
|
|
77
|
+
_global_default_factory = factory
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def resolve_agent_factory(agent_factory: AgentFactory | None = None) -> AgentFactory:
|
|
81
|
+
"""Resolve an agent factory, falling back to registered default."""
|
|
82
|
+
return agent_factory or get_default_agent_factory()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def create_agent(
|
|
86
|
+
model: str = "",
|
|
87
|
+
api_key: str = "",
|
|
88
|
+
spells: str | Sequence[str] | None = None,
|
|
89
|
+
extension_dir: str | None = None,
|
|
90
|
+
tome_dir: str | None = None,
|
|
91
|
+
resume: str | None = None,
|
|
92
|
+
provider: str | None = None,
|
|
93
|
+
temperature: float = 0.7,
|
|
94
|
+
max_tokens: int = 4096,
|
|
95
|
+
contemplation: str = "medium",
|
|
96
|
+
agent_name: str = DEFAULT_AGENT_NAME,
|
|
97
|
+
agent_factory: AgentFactory | None = None,
|
|
98
|
+
**kwargs: Any,
|
|
99
|
+
) -> MvgeAgent:
|
|
100
|
+
"""Resolve environment, create an agent via factory, and initialize it."""
|
|
101
|
+
if api_key or (model and not model.startswith("ollama/")):
|
|
102
|
+
validate_api_key(api_key, model_id=model)
|
|
103
|
+
spells_list: list[str] | None = None
|
|
104
|
+
if isinstance(spells, str):
|
|
105
|
+
spells_list = [s.strip() for s in spells.split(",") if s.strip()]
|
|
106
|
+
elif isinstance(spells, Sequence):
|
|
107
|
+
spells_list = list(spells)
|
|
108
|
+
|
|
109
|
+
overrides: dict[str, Any] = {}
|
|
110
|
+
if model:
|
|
111
|
+
overrides["model"] = model
|
|
112
|
+
if temperature is not None:
|
|
113
|
+
overrides["temperature"] = temperature
|
|
114
|
+
if max_tokens is not None:
|
|
115
|
+
overrides["max_tokens"] = max_tokens
|
|
116
|
+
if contemplation:
|
|
117
|
+
overrides["contemplation_level"] = contemplation
|
|
118
|
+
|
|
119
|
+
env = MvgeEnvironment.resolve(
|
|
120
|
+
agent_name,
|
|
121
|
+
extension_dir=extension_dir,
|
|
122
|
+
overrides=overrides if overrides else None,
|
|
123
|
+
project_dir=cli_project_dir(),
|
|
124
|
+
)
|
|
125
|
+
factory = resolve_agent_factory(agent_factory)
|
|
126
|
+
factory_kwargs: dict[str, Any] = {
|
|
127
|
+
"api_key": api_key,
|
|
128
|
+
"name": agent_name,
|
|
129
|
+
"model": model,
|
|
130
|
+
"extension_dir": extension_dir,
|
|
131
|
+
"tome_dir": Path(tome_dir) if tome_dir else None,
|
|
132
|
+
"tome_resume": resume,
|
|
133
|
+
"provider_name": provider,
|
|
134
|
+
"temperature": temperature,
|
|
135
|
+
"max_tokens": max_tokens,
|
|
136
|
+
"contemplation": contemplation,
|
|
137
|
+
"environment": env,
|
|
138
|
+
**kwargs,
|
|
139
|
+
}
|
|
140
|
+
if spells_list is not None:
|
|
141
|
+
factory_kwargs["spells"] = spells_list
|
|
142
|
+
|
|
143
|
+
agent = factory(**factory_kwargs)
|
|
144
|
+
await agent.initialize()
|
|
145
|
+
return agent
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Host-side bind/unbind adapter for the Approval Rune presenter slot.
|
|
2
|
+
|
|
3
|
+
Canonical engine slot shape (``RuneRunner`` in ``mvgeos_runes``; this
|
|
4
|
+
adapter duck-types against it)::
|
|
5
|
+
|
|
6
|
+
runner.set_approval_presenter(presenter) -> None
|
|
7
|
+
runner.clear_approval_presenter() -> None
|
|
8
|
+
|
|
9
|
+
where ``presenter`` satisfies ``mvgeos_core.approval.ApprovalPresenter``
|
|
10
|
+
(``await presenter(request)``). Binding is host-privileged: only the host
|
|
11
|
+
(CLI/GUI) calls the setter. It is never exposed through ``RuneAPI`` —
|
|
12
|
+
otherwise any rune could install an auto-approver. With no presenter
|
|
13
|
+
bound, the rune's ``request_approval`` denies (fail closed).
|
|
14
|
+
|
|
15
|
+
Also carries the one-run ``--approval-mode`` helpers: the flag is never
|
|
16
|
+
persisted, and a warning prints before channeling begins whenever it is
|
|
17
|
+
explicitly set to a non-deny value. Environment variables never grant
|
|
18
|
+
approval and are not consulted here.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Iterator
|
|
24
|
+
from contextlib import contextmanager, suppress
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from mvgeos_cli.approval_types import ApprovalMode, ApprovalPresenter
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"NO_SLOT_WARNING",
|
|
31
|
+
"approval_mode_notice",
|
|
32
|
+
"approval_presenter_bound",
|
|
33
|
+
"bind_approval_presenter",
|
|
34
|
+
"resolve_approval_mode",
|
|
35
|
+
"unbind_approval_presenter",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
#: Warned when the engine runner has no approval slot yet. Execution stays
|
|
39
|
+
#: fail-closed: with no presenter bound the rune denies gated casts.
|
|
40
|
+
NO_SLOT_WARNING = (
|
|
41
|
+
"Approval presenter could not bind: the engine runner has no approval "
|
|
42
|
+
"slot. Gated spell casts will be denied."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
_VALID_MODES: tuple[ApprovalMode, ...] = ("deny", "prompt", "allow-all")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def resolve_approval_mode(value: str | None) -> ApprovalMode:
|
|
49
|
+
"""Resolve the one-run ``--approval-mode`` flag value.
|
|
50
|
+
|
|
51
|
+
``None`` (flag not passed) means ``"prompt"``: the spec default is prompt
|
|
52
|
+
on a TTY and deny without one, and the presenter itself denies when
|
|
53
|
+
there is no terminal. Unknown values are a loud error, never a guess.
|
|
54
|
+
"""
|
|
55
|
+
if value is None:
|
|
56
|
+
return "prompt"
|
|
57
|
+
if value in _VALID_MODES:
|
|
58
|
+
return value
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Unknown approval mode: {value!r} (expected one of: {', '.join(_VALID_MODES)})"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def approval_mode_notice(mode: ApprovalMode) -> str | None:
|
|
65
|
+
"""Human-readable notice for an explicitly set approval mode.
|
|
66
|
+
|
|
67
|
+
Printed before channeling begins. Always returns a line for explicit
|
|
68
|
+
modes so the Summoner sees what the run will do.
|
|
69
|
+
"""
|
|
70
|
+
if mode == "allow-all":
|
|
71
|
+
return (
|
|
72
|
+
"WARNING: --approval-mode=allow-all: every mutating spell cast "
|
|
73
|
+
"will be approved automatically for this run only. "
|
|
74
|
+
"This is never persisted."
|
|
75
|
+
)
|
|
76
|
+
if mode == "deny":
|
|
77
|
+
return "Approval mode: deny. All gated spell casts will be denied for this run."
|
|
78
|
+
return (
|
|
79
|
+
"Approval mode: prompt. Each mutating spell cast asks for approval; "
|
|
80
|
+
"without a terminal, casts are denied."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def bind_approval_presenter(runner: Any, presenter: ApprovalPresenter) -> bool:
|
|
85
|
+
"""Bind ``presenter`` to the engine-owned runner's approval slot.
|
|
86
|
+
|
|
87
|
+
Returns True when bound. Returns False (no raise) when the runner has
|
|
88
|
+
no slot yet; the caller warns and execution stays fail-closed because
|
|
89
|
+
the rune denies with no presenter bound.
|
|
90
|
+
"""
|
|
91
|
+
setter = getattr(runner, "set_approval_presenter", None)
|
|
92
|
+
if not callable(setter):
|
|
93
|
+
return False
|
|
94
|
+
setter(presenter)
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def unbind_approval_presenter(
|
|
99
|
+
runner: Any, presenter: ApprovalPresenter | None = None
|
|
100
|
+
) -> bool:
|
|
101
|
+
"""Unbind the presenter, resolving pending requests as denied.
|
|
102
|
+
|
|
103
|
+
Closes ``presenter`` when given (its ``close()`` denies all further
|
|
104
|
+
requests) before clearing the slot; the engine also fails every
|
|
105
|
+
in-flight approval as denied when the slot changes. Returns True when
|
|
106
|
+
a slot was present, False otherwise.
|
|
107
|
+
"""
|
|
108
|
+
clearer = getattr(runner, "clear_approval_presenter", None)
|
|
109
|
+
setter = getattr(runner, "set_approval_presenter", None)
|
|
110
|
+
if not callable(clearer):
|
|
111
|
+
if not callable(setter):
|
|
112
|
+
return False
|
|
113
|
+
# Legacy slot without clear: None-ing the setter is best-effort.
|
|
114
|
+
if presenter is not None:
|
|
115
|
+
close = getattr(presenter, "close", None)
|
|
116
|
+
if callable(close):
|
|
117
|
+
with suppress(Exception):
|
|
118
|
+
close()
|
|
119
|
+
with suppress(Exception):
|
|
120
|
+
setter(None)
|
|
121
|
+
return True
|
|
122
|
+
if presenter is not None:
|
|
123
|
+
close = getattr(presenter, "close", None)
|
|
124
|
+
if callable(close):
|
|
125
|
+
with suppress(Exception):
|
|
126
|
+
close()
|
|
127
|
+
clearer()
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@contextmanager
|
|
132
|
+
def approval_presenter_bound(
|
|
133
|
+
runner: Any, presenter: ApprovalPresenter
|
|
134
|
+
) -> Iterator[bool]:
|
|
135
|
+
"""Bind the presenter for the wrapped run; unbind on exit.
|
|
136
|
+
|
|
137
|
+
Yields True when the presenter was bound, False when the runner has no
|
|
138
|
+
approval slot (execution stays fail-closed). Unbinds even on error.
|
|
139
|
+
"""
|
|
140
|
+
bound = bind_approval_presenter(runner, presenter)
|
|
141
|
+
try:
|
|
142
|
+
yield bound
|
|
143
|
+
finally:
|
|
144
|
+
if bound:
|
|
145
|
+
unbind_approval_presenter(runner, presenter)
|