miniouto 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.
- miniouto/__init__.py +3 -0
- miniouto/cli/__init__.py +70 -0
- miniouto/cli/chat.py +77 -0
- miniouto/cli/provider.py +272 -0
- miniouto/cli/skill.py +51 -0
- miniouto/cli/style.py +112 -0
- miniouto/cli/tui.py +1257 -0
- miniouto/core/__init__.py +13 -0
- miniouto/core/chat.py +323 -0
- miniouto/core/context.py +184 -0
- miniouto/core/events.py +124 -0
- miniouto/core/lma.py +139 -0
- miniouto/core/providers.py +134 -0
- miniouto/core/runtime.py +398 -0
- miniouto/default_style/claude.md +333 -0
- miniouto/default_style/codebuff.md +244 -0
- miniouto/default_style/codex.md +376 -0
- miniouto/default_style/default.md +111 -0
- miniouto/default_style/oh-my-opencode.md +306 -0
- miniouto/default_style/opencode.md +255 -0
- miniouto/paths_runtime.py +8 -0
- miniouto/storage/__init__.py +3 -0
- miniouto/storage/paths.py +37 -0
- miniouto/storage/providers.py +70 -0
- miniouto/storage/sessions.py +88 -0
- miniouto/storage/settings.py +46 -0
- miniouto/storage/skills.py +98 -0
- miniouto/storage/styles.py +242 -0
- miniouto/storage/toml_io.py +23 -0
- miniouto/tools/__init__.py +12 -0
- miniouto/tools/_normalize.py +79 -0
- miniouto/tools/bash.py +82 -0
- miniouto/tools/delete.py +29 -0
- miniouto/tools/edit.py +221 -0
- miniouto/tools/media.py +140 -0
- miniouto/tools/registry.py +279 -0
- miniouto/tools/write.py +90 -0
- miniouto-0.1.0.dist-info/METADATA +125 -0
- miniouto-0.1.0.dist-info/RECORD +42 -0
- miniouto-0.1.0.dist-info/WHEEL +4 -0
- miniouto-0.1.0.dist-info/entry_points.txt +2 -0
- miniouto-0.1.0.dist-info/licenses/LICENSE +201 -0
miniouto/__init__.py
ADDED
miniouto/cli/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""miniouto CLI entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from ..storage import paths
|
|
9
|
+
from . import provider as provider_module
|
|
10
|
+
from . import skill as skill_module
|
|
11
|
+
from . import style as style_module
|
|
12
|
+
from . import tui as tui_module
|
|
13
|
+
from .chat import chat_cmd
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="miniouto",
|
|
17
|
+
help="A minimal, file-driven CLI agent harness built on coreouto.",
|
|
18
|
+
no_args_is_help=False,
|
|
19
|
+
invoke_without_command=True,
|
|
20
|
+
add_completion=False,
|
|
21
|
+
)
|
|
22
|
+
app.add_typer(provider_module.app, name="provider")
|
|
23
|
+
app.add_typer(style_module.app, name="style")
|
|
24
|
+
app.add_typer(skill_module.app, name="skill")
|
|
25
|
+
app.command("chat", help="Run a single chat turn.")(chat_cmd)
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.callback()
|
|
31
|
+
def _root(
|
|
32
|
+
ctx: typer.Context,
|
|
33
|
+
version: bool = typer.Option(False, "--version", help="Show version and exit."),
|
|
34
|
+
) -> None:
|
|
35
|
+
paths.ensure_dirs()
|
|
36
|
+
if version:
|
|
37
|
+
from .. import __version__
|
|
38
|
+
|
|
39
|
+
console.print(f"miniouto {__version__}")
|
|
40
|
+
raise typer.Exit()
|
|
41
|
+
if ctx.invoked_subcommand is None:
|
|
42
|
+
tui_module.run_tui()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.command("status")
|
|
46
|
+
def status() -> None:
|
|
47
|
+
"""Show current configuration."""
|
|
48
|
+
|
|
49
|
+
from ..storage import providers as provider_store
|
|
50
|
+
from ..storage import sessions as session_store
|
|
51
|
+
from ..storage import settings as settings_store
|
|
52
|
+
from ..storage import skills as skill_store
|
|
53
|
+
from ..storage import styles as style_store
|
|
54
|
+
|
|
55
|
+
s = settings_store.load()
|
|
56
|
+
active_provider = provider_store.get(s.provider) if s.provider else None
|
|
57
|
+
default_model = active_provider.default_model if active_provider else ""
|
|
58
|
+
console.print(f"[bold]Default provider:[/bold] {s.provider or '-'}")
|
|
59
|
+
console.print(f"[bold]Default model:[/bold] {default_model or '- (use chat --model)'}")
|
|
60
|
+
console.print(f"[bold]Active style:[/bold] {s.style or '-'}")
|
|
61
|
+
console.print(f"[bold]Session:[/bold] {s.session or '-'}")
|
|
62
|
+
console.print(f"[bold]Storage:[/bold] {paths.ROOT}")
|
|
63
|
+
console.print(f"[bold]Providers:[/bold] {', '.join(provider_store.load_all()) or '-'}")
|
|
64
|
+
console.print(f"[bold]Styles:[/bold] {', '.join(style_store.list_styles()) or '-'}")
|
|
65
|
+
console.print(f"[bold]Skills:[/bold] {', '.join(s.name for s in skill_store.list_skills()) or '-'}")
|
|
66
|
+
console.print(f"[bold]Sessions:[/bold] {', '.join(session_store.list_sessions()) or '-'}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
app()
|
miniouto/cli/chat.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Single-shot chat command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import sys
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from ..core.chat import ChatOptions, run_chat
|
|
12
|
+
from ..core.events import ConsoleEventSink
|
|
13
|
+
from ..storage import settings as settings_store
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def chat_cmd(
|
|
17
|
+
prompt: str = typer.Argument(..., help="Prompt to send to the agent."),
|
|
18
|
+
name: str | None = typer.Option(
|
|
19
|
+
None, "--name", help="Session name. Without --name and --continue, a fresh session is generated each call."
|
|
20
|
+
),
|
|
21
|
+
provider: str | None = typer.Option(None, "--provider", help="Override the active provider."),
|
|
22
|
+
model: str | None = typer.Option(None, "--model", help="Override the default model."),
|
|
23
|
+
style: str | None = typer.Option(None, "--style", help="Override the active style."),
|
|
24
|
+
max_tokens: int | None = typer.Option(None, "--max-tokens", help="Cap output tokens."),
|
|
25
|
+
temperature: float | None = typer.Option(None, "--temperature", help="Sampling temperature."),
|
|
26
|
+
continue_session: bool = typer.Option(
|
|
27
|
+
False, "--continue", "-c", help="Prepend the session's previous history."
|
|
28
|
+
),
|
|
29
|
+
answer_only: bool = typer.Option(
|
|
30
|
+
False, "--answer-only", "-a",
|
|
31
|
+
help="Print only the final answer. Suppresses the session marker, loop events, and finish marker.",
|
|
32
|
+
),
|
|
33
|
+
with_session: bool = typer.Option(
|
|
34
|
+
False, "--with-session",
|
|
35
|
+
help="Print only the session marker and the final answer. Suppresses loop events and the finish marker.",
|
|
36
|
+
),
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Run one prompt and print the agent's reply."""
|
|
39
|
+
|
|
40
|
+
if answer_only and with_session:
|
|
41
|
+
typer.secho(
|
|
42
|
+
"✗ --answer-only and --with-session are mutually exclusive.", err=True, fg="red"
|
|
43
|
+
)
|
|
44
|
+
raise typer.Exit(1)
|
|
45
|
+
|
|
46
|
+
if continue_session:
|
|
47
|
+
session_name = name or settings_store.load().session or "default"
|
|
48
|
+
elif name is not None:
|
|
49
|
+
session_name = name
|
|
50
|
+
else:
|
|
51
|
+
# Fresh session each call: timestamp + short UUID keeps names
|
|
52
|
+
# unique even within the same second and sortable by recency,
|
|
53
|
+
# so the previous chat's `settings.session` doesn't bleed in.
|
|
54
|
+
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
55
|
+
suffix = uuid.uuid4().hex[:6]
|
|
56
|
+
session_name = f"chat-{ts}-{suffix}"
|
|
57
|
+
settings_store.update(session=session_name)
|
|
58
|
+
|
|
59
|
+
opts = ChatOptions(
|
|
60
|
+
prompt=prompt,
|
|
61
|
+
session=session_name,
|
|
62
|
+
provider=provider,
|
|
63
|
+
model=model,
|
|
64
|
+
style=style,
|
|
65
|
+
max_tokens=max_tokens,
|
|
66
|
+
temperature=temperature,
|
|
67
|
+
continue_session=continue_session,
|
|
68
|
+
)
|
|
69
|
+
# The sink handles all output: braille spinner + loop events share
|
|
70
|
+
# stdout (Rich's Live display owns one channel and keeps them
|
|
71
|
+
# vertically separated). The session marker is printed up front so
|
|
72
|
+
# callers know which session the output belongs to.
|
|
73
|
+
quiet = answer_only or with_session
|
|
74
|
+
if not answer_only:
|
|
75
|
+
sys.stdout.write(f"------{session_name}------\n")
|
|
76
|
+
sys.stdout.flush()
|
|
77
|
+
run_chat(opts, sink=ConsoleEventSink(quiet=quiet))
|
miniouto/cli/provider.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Provider subcommands.
|
|
2
|
+
|
|
3
|
+
Three groups:
|
|
4
|
+
|
|
5
|
+
* **catalog** browse / add (sourced from the upstream lma catalog at
|
|
6
|
+
https://lma.blp.sh, but exposed to users as the "catalog"): `providers`,
|
|
7
|
+
`models`, `add`.
|
|
8
|
+
* **storage** ops on already-configured providers: `list`, `remove`, `default`.
|
|
9
|
+
* **custom** manual config (no catalog lookup): `provider custom add`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
from ..core import lma as catalog_api
|
|
19
|
+
from ..core.providers import SUPPORTED_FORMATS, add_provider_from_lma, sdk_to_format
|
|
20
|
+
from ..storage import paths
|
|
21
|
+
from ..storage import providers as provider_store
|
|
22
|
+
from ..storage import settings as settings_store
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(help="Manage LLM providers (catalog browse + custom config).")
|
|
25
|
+
custom_app = typer.Typer(help="Manually configure a custom provider (advanced).")
|
|
26
|
+
app.add_typer(custom_app, name="custom")
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
FORMAT_HELP = (
|
|
31
|
+
"API compatibility: "
|
|
32
|
+
"openai (OpenAI Chat Completions), "
|
|
33
|
+
"openai-response (OpenAI Responses API), "
|
|
34
|
+
"anthropic (Anthropic Messages), "
|
|
35
|
+
"google (Google Generative AI / Gemini)."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ─── catalog commands ────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("providers")
|
|
43
|
+
def providers_cmd() -> None:
|
|
44
|
+
"""List every provider available in the catalog."""
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
providers = catalog_api.list_providers()
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
console.print(f"[red]✗[/red] Failed to reach catalog: {exc}")
|
|
50
|
+
raise typer.Exit(code=1) from exc
|
|
51
|
+
if not providers:
|
|
52
|
+
console.print("[yellow]No providers returned by the catalog.[/yellow]")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
table = Table(
|
|
56
|
+
title=f"Catalog providers ({len(providers)})",
|
|
57
|
+
show_header=True,
|
|
58
|
+
header_style="bold",
|
|
59
|
+
)
|
|
60
|
+
table.add_column("Name", style="cyan")
|
|
61
|
+
table.add_column("SDK")
|
|
62
|
+
table.add_column("API URL")
|
|
63
|
+
table.add_column("miniouto format", style="magenta")
|
|
64
|
+
table.add_column("Addable?", justify="center")
|
|
65
|
+
for p in providers:
|
|
66
|
+
name = p.get("name", "?")
|
|
67
|
+
sdk = p.get("sdk") or "-"
|
|
68
|
+
api = p.get("api") or "-"
|
|
69
|
+
fmt, _ = sdk_to_format(sdk, api)
|
|
70
|
+
fmt_str = fmt or "[red]unsupported[/red]"
|
|
71
|
+
addable = "[green]✓[/green]" if fmt else "[dim]✗[/dim]"
|
|
72
|
+
table.add_row(name, sdk, api, fmt_str, addable)
|
|
73
|
+
console.print(table)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command("models")
|
|
77
|
+
def models_cmd(
|
|
78
|
+
provider_name: str = typer.Argument(
|
|
79
|
+
..., help="Provider name (fuzzy match, e.g. 'anthropic', 'open ai')."
|
|
80
|
+
),
|
|
81
|
+
) -> None:
|
|
82
|
+
"""List every model the catalog knows for a provider."""
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
models = catalog_api.list_models(provider_name)
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
console.print(f"[red]✗[/red] Failed to reach catalog: {exc}")
|
|
88
|
+
raise typer.Exit(code=1) from exc
|
|
89
|
+
if not models:
|
|
90
|
+
console.print(f"[yellow]No models returned for {provider_name!r}.[/yellow]")
|
|
91
|
+
raise typer.Exit(code=1)
|
|
92
|
+
table = Table(
|
|
93
|
+
title=f"Catalog models for {provider_name!r} ({len(models)})",
|
|
94
|
+
show_header=True,
|
|
95
|
+
header_style="bold",
|
|
96
|
+
)
|
|
97
|
+
table.add_column("ID", style="cyan")
|
|
98
|
+
table.add_column("Name")
|
|
99
|
+
for m in models:
|
|
100
|
+
table.add_row(m.get("id", "?"), m.get("name", "?"))
|
|
101
|
+
console.print(table)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command("add")
|
|
105
|
+
def add_cmd(
|
|
106
|
+
provider_name: str = typer.Argument(
|
|
107
|
+
..., help="Catalog provider name (e.g. 'OpenAI', 'Anthropic', 'GitHub Copilot')."
|
|
108
|
+
),
|
|
109
|
+
api_key: str = typer.Option(..., "--api-key", help="API key for the provider."),
|
|
110
|
+
default_model: str = typer.Option(
|
|
111
|
+
"",
|
|
112
|
+
"--default-model",
|
|
113
|
+
help="Default model id. If empty, the first model the catalog lists for this provider is used.",
|
|
114
|
+
),
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Add a provider from the catalog by name + API key.
|
|
117
|
+
|
|
118
|
+
For manual configuration (custom base URL / format), use
|
|
119
|
+
`miniouto provider custom add` instead.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
catalog_provider = catalog_api.find_provider(provider_name)
|
|
123
|
+
if catalog_provider is None:
|
|
124
|
+
console.print(
|
|
125
|
+
f"[red]✗[/red] No catalog provider matched {provider_name!r}. "
|
|
126
|
+
"Run `miniouto provider providers` to see the catalog."
|
|
127
|
+
)
|
|
128
|
+
raise typer.Exit(code=1)
|
|
129
|
+
|
|
130
|
+
name = catalog_api.slugify(catalog_provider["name"])
|
|
131
|
+
sdk = catalog_provider.get("sdk")
|
|
132
|
+
api = catalog_provider.get("api")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
provider = add_provider_from_lma(
|
|
136
|
+
name=name,
|
|
137
|
+
api_key=api_key,
|
|
138
|
+
sdk=sdk,
|
|
139
|
+
api=api,
|
|
140
|
+
default_model=default_model,
|
|
141
|
+
)
|
|
142
|
+
except ValueError as exc:
|
|
143
|
+
console.print(f"[red]✗[/red] {exc}")
|
|
144
|
+
raise typer.Exit(code=1) from exc
|
|
145
|
+
|
|
146
|
+
if not provider.default_model:
|
|
147
|
+
try:
|
|
148
|
+
models = catalog_api.list_models(catalog_provider["name"])
|
|
149
|
+
if models:
|
|
150
|
+
provider = add_provider_from_lma(
|
|
151
|
+
name=name,
|
|
152
|
+
api_key=api_key,
|
|
153
|
+
sdk=sdk,
|
|
154
|
+
api=api,
|
|
155
|
+
default_model=models[0].get("id", ""),
|
|
156
|
+
)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
paths.ensure_dirs()
|
|
161
|
+
if provider_store.get(name) is not None:
|
|
162
|
+
console.print(
|
|
163
|
+
f"[yellow]![/yellow] Provider [bold]{name}[/bold] already exists; overwriting."
|
|
164
|
+
)
|
|
165
|
+
provider_store.upsert(provider)
|
|
166
|
+
console.print(
|
|
167
|
+
f"[green]✓[/green] Added provider [bold]{name}[/bold] "
|
|
168
|
+
f"({provider.api_format}, default-model={provider.default_model or '-'})."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ─── storage commands (unchanged behavior) ───────────────────────────────────
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.command("list")
|
|
176
|
+
def list_cmd() -> None:
|
|
177
|
+
"""List configured providers."""
|
|
178
|
+
|
|
179
|
+
rows = provider_store.load_all()
|
|
180
|
+
if not rows:
|
|
181
|
+
console.print(
|
|
182
|
+
"[yellow]No providers configured.[/yellow] "
|
|
183
|
+
"Run `miniouto provider add <name>` or `miniouto provider custom add`."
|
|
184
|
+
)
|
|
185
|
+
return
|
|
186
|
+
current = settings_store.load().provider
|
|
187
|
+
table = Table(title="Providers", show_header=True, header_style="bold")
|
|
188
|
+
table.add_column("Name", style="cyan")
|
|
189
|
+
table.add_column("Type")
|
|
190
|
+
table.add_column("Format")
|
|
191
|
+
table.add_column("Base URL")
|
|
192
|
+
table.add_column("Default Model")
|
|
193
|
+
table.add_column("Default", justify="center")
|
|
194
|
+
for p in rows.values():
|
|
195
|
+
marker = "[green]●[/green]" if p.name == current else ""
|
|
196
|
+
kind = "custom" if p.source == "custom" else "catalog"
|
|
197
|
+
table.add_row(
|
|
198
|
+
p.name,
|
|
199
|
+
kind,
|
|
200
|
+
p.api_format,
|
|
201
|
+
p.base_url or "-",
|
|
202
|
+
p.default_model or "-",
|
|
203
|
+
marker,
|
|
204
|
+
)
|
|
205
|
+
console.print(table)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.command("remove")
|
|
209
|
+
def remove(name: str) -> None:
|
|
210
|
+
"""Remove a provider by name."""
|
|
211
|
+
|
|
212
|
+
if provider_store.remove(name):
|
|
213
|
+
console.print(f"[green]✓[/green] Removed provider [bold]{name}[/bold].")
|
|
214
|
+
else:
|
|
215
|
+
console.print(f"[red]✗[/red] Provider [bold]{name}[/bold] does not exist.")
|
|
216
|
+
raise typer.Exit(code=1)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@app.command("default")
|
|
220
|
+
def default(name: str) -> None:
|
|
221
|
+
"""Set the default provider for chat."""
|
|
222
|
+
|
|
223
|
+
if provider_store.get(name) is None:
|
|
224
|
+
console.print(f"[red]✗[/red] Provider [bold]{name}[/bold] is not configured.")
|
|
225
|
+
raise typer.Exit(code=1)
|
|
226
|
+
settings_store.update(provider=name)
|
|
227
|
+
console.print(f"[green]✓[/green] Default provider is now [bold]{name}[/bold].")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ─── custom subcommands ──────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@custom_app.command("add")
|
|
234
|
+
def add_custom(
|
|
235
|
+
name: str = typer.Option(..., "--name", help="Provider identifier (e.g. openai, minimax)."),
|
|
236
|
+
api_format: str = typer.Option("openai", "--format", help=FORMAT_HELP),
|
|
237
|
+
base_url: str = typer.Option(
|
|
238
|
+
"", "--base-url",
|
|
239
|
+
help="Base URL of the endpoint. For OpenAI-compatible services "
|
|
240
|
+
"(LocalAI, vLLM, MiniMax, Zhipu, Moonshot, etc.) use the OpenAI or "
|
|
241
|
+
"openai-response format with the provider's base URL. Anthropic "
|
|
242
|
+
"format works with the Anthropic SDK and any compatible proxy. "
|
|
243
|
+
"Google format accepts an api_endpoint via client_options.",
|
|
244
|
+
),
|
|
245
|
+
api_key: str = typer.Option("", "--api-key", help="API key (omit to read from env at call time)."),
|
|
246
|
+
default_model: str = typer.Option(
|
|
247
|
+
"", "--default-model", help="Default model used when chat --model is not given."
|
|
248
|
+
),
|
|
249
|
+
) -> None:
|
|
250
|
+
"""Manually add or update a custom provider.
|
|
251
|
+
|
|
252
|
+
For catalog providers (OpenAI, Anthropic, etc.), use
|
|
253
|
+
`miniouto provider add <name>` instead — it auto-fills base URL, format,
|
|
254
|
+
and default model.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
if api_format not in SUPPORTED_FORMATS:
|
|
258
|
+
console.print(
|
|
259
|
+
f"[red]✗[/red] Unknown format {api_format!r}. "
|
|
260
|
+
f"Supported: {', '.join(SUPPORTED_FORMATS)}."
|
|
261
|
+
)
|
|
262
|
+
raise typer.Exit(code=1)
|
|
263
|
+
paths.ensure_dirs()
|
|
264
|
+
provider = provider_store.Provider(
|
|
265
|
+
name=name,
|
|
266
|
+
api_format=api_format,
|
|
267
|
+
base_url=base_url,
|
|
268
|
+
api_key=api_key,
|
|
269
|
+
default_model=default_model,
|
|
270
|
+
)
|
|
271
|
+
provider_store.upsert(provider)
|
|
272
|
+
console.print(f"[green]✓[/green] Saved custom provider [bold]{name}[/bold] ({api_format}).")
|
miniouto/cli/skill.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Skill CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..storage import skills as skill_store
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Manage agent skills.")
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("list")
|
|
16
|
+
def list_cmd() -> None:
|
|
17
|
+
"""List all available skills."""
|
|
18
|
+
|
|
19
|
+
skills = skill_store.list_skills()
|
|
20
|
+
if not skills:
|
|
21
|
+
console.print("[yellow]No skills found.[/yellow] Check ~/.agents/skills/")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
table = Table(title="Available Skills", show_header=True, header_style="bold")
|
|
25
|
+
table.add_column("Name", style="cyan")
|
|
26
|
+
table.add_column("Description")
|
|
27
|
+
|
|
28
|
+
for skill in skills:
|
|
29
|
+
desc = skill.description[:80] + "..." if len(skill.description) > 80 else skill.description
|
|
30
|
+
table.add_row(skill.name, desc)
|
|
31
|
+
|
|
32
|
+
console.print(table)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command("show")
|
|
36
|
+
def show(name: str) -> None:
|
|
37
|
+
"""Show skill content."""
|
|
38
|
+
|
|
39
|
+
skill = skill_store.get_skill(name)
|
|
40
|
+
if skill is None:
|
|
41
|
+
console.print(f"[red]✗[/red] Skill [bold]{name}[/bold] not found.")
|
|
42
|
+
raise typer.Exit(code=1)
|
|
43
|
+
|
|
44
|
+
console.print(f"[bold]Name:[/bold] {skill.name}")
|
|
45
|
+
console.print(f"[bold]Description:[/bold] {skill.description}")
|
|
46
|
+
if skill.license:
|
|
47
|
+
console.print(f"[bold]License:[/bold] {skill.license}")
|
|
48
|
+
if skill.allowed_tools:
|
|
49
|
+
console.print(f"[bold]Allowed Tools:[/bold] {skill.allowed_tools}")
|
|
50
|
+
console.print()
|
|
51
|
+
console.print(skill.content)
|
miniouto/cli/style.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Style subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from ..storage import paths
|
|
9
|
+
from ..storage import settings as settings_store
|
|
10
|
+
from ..storage import styles as style_store
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Manage agent style documents.")
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("list")
|
|
17
|
+
def list_cmd() -> None:
|
|
18
|
+
"""List installed style documents."""
|
|
19
|
+
|
|
20
|
+
names = style_store.list_styles()
|
|
21
|
+
if not names:
|
|
22
|
+
console.print("[yellow]No styles installed.[/yellow]")
|
|
23
|
+
return
|
|
24
|
+
current = settings_store.load().style
|
|
25
|
+
for name in names:
|
|
26
|
+
marker = " [green]●[/green]" if name == current else ""
|
|
27
|
+
console.print(f" - {name}{marker}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command("set")
|
|
31
|
+
def set_cmd(name: str) -> None:
|
|
32
|
+
"""Activate an installed style as the default."""
|
|
33
|
+
|
|
34
|
+
if style_store.read(name) is None:
|
|
35
|
+
console.print(f"[red]✗[/red] Style [bold]{name}[/bold] is not installed.")
|
|
36
|
+
raise typer.Exit(code=1)
|
|
37
|
+
settings_store.update(style=name)
|
|
38
|
+
console.print(f"[green]✓[/green] Active style is now [bold]{name}[/bold].")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("add")
|
|
42
|
+
def add(
|
|
43
|
+
repo_url: str = typer.Argument(..., help="Git host URL whose /style-md/ directory defines styles."),
|
|
44
|
+
name: str | None = typer.Option(
|
|
45
|
+
None,
|
|
46
|
+
"--name",
|
|
47
|
+
help="Override style name (otherwise the file's basename is used).",
|
|
48
|
+
),
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Add styles by fetching /style-md/ from a remote repository."""
|
|
51
|
+
|
|
52
|
+
paths.ensure_dirs()
|
|
53
|
+
try:
|
|
54
|
+
added = style_store.add_from_repo(repo_url, name_override=name)
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
console.print(f"[red]✗[/red] Failed to fetch styles: {exc}")
|
|
57
|
+
raise typer.Exit(code=1) from exc
|
|
58
|
+
console.print(f"[green]✓[/green] Added/updated styles: {', '.join(added)}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command("update")
|
|
62
|
+
def update_cmd() -> None:
|
|
63
|
+
"""Refresh every style to its latest source.
|
|
64
|
+
|
|
65
|
+
Re-seeds all bundled styles from the miniouto package, then re-fetches
|
|
66
|
+
every repo previously added via ``style add``. Same-name files are
|
|
67
|
+
overwritten in place. Styles you created by hand (no matching bundled
|
|
68
|
+
template and no recorded repo) are left untouched.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
paths.ensure_dirs()
|
|
72
|
+
refreshed_bundled = style_store.bundled_style_names()
|
|
73
|
+
|
|
74
|
+
repos = style_store.list_repos()
|
|
75
|
+
failed: list[tuple[str, str]] = []
|
|
76
|
+
refreshed_repos: list[str] = []
|
|
77
|
+
for url in repos:
|
|
78
|
+
try:
|
|
79
|
+
added = style_store.add_from_repo(url)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
failed.append((url, str(exc)))
|
|
82
|
+
continue
|
|
83
|
+
refreshed_repos.extend(added)
|
|
84
|
+
|
|
85
|
+
console.print(
|
|
86
|
+
f"[green]✓[/green] Refreshed bundled styles: "
|
|
87
|
+
f"{', '.join(refreshed_bundled) or '-'}"
|
|
88
|
+
)
|
|
89
|
+
if repos:
|
|
90
|
+
ok = sorted(set(refreshed_repos))
|
|
91
|
+
console.print(
|
|
92
|
+
f"[green]✓[/green] Re-fetched {len(repos)} repo(s): "
|
|
93
|
+
f"{', '.join(ok) or 'no .md files'}"
|
|
94
|
+
)
|
|
95
|
+
for url, err in failed:
|
|
96
|
+
console.print(f"[red]✗[/red] Failed to update {url}: {err}")
|
|
97
|
+
elif not repos:
|
|
98
|
+
console.print(
|
|
99
|
+
"[dim]No repo styles to update. Use `style add <repo-url>` "
|
|
100
|
+
"to track a repo.[/dim]"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command("show")
|
|
105
|
+
def show(name: str) -> None:
|
|
106
|
+
"""Print the contents of a style document."""
|
|
107
|
+
|
|
108
|
+
content = style_store.read(name)
|
|
109
|
+
if content is None:
|
|
110
|
+
console.print(f"[red]✗[/red] Style [bold]{name}[/bold] is not installed.")
|
|
111
|
+
raise typer.Exit(code=1)
|
|
112
|
+
console.print(content)
|