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.
@@ -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,3 @@
1
+ from mvgeos_core.constants import DEFAULT_MODEL
2
+
3
+ __all__ = ["DEFAULT_MODEL"]
@@ -0,0 +1,6 @@
1
+ """Entrypoint for python -m mvgeos_cli."""
2
+
3
+ from mvgeos_cli.main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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)