muapi-cli 0.2.5__tar.gz → 0.2.7__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.
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/.github/workflows/release.yml +2 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/PKG-INFO +6 -1
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/README.md +5 -0
- muapi_cli-0.2.7/integrations/langchain/examples/creative-agent/AGENTS.md +37 -0
- muapi_cli-0.2.7/integrations/langchain/examples/creative-agent/README.md +81 -0
- muapi_cli-0.2.7/integrations/langchain/examples/creative-agent/creative_agent.py +203 -0
- muapi_cli-0.2.7/integrations/langchain/examples/creative-agent/pyproject.toml +15 -0
- muapi_cli-0.2.7/integrations/langchain/examples/creative-agent/skills/generate-asset/SKILL.md +37 -0
- muapi_cli-0.2.7/integrations/langchain/examples/creative-agent/skills/run-skill/SKILL.md +30 -0
- muapi_cli-0.2.7/integrations/langchain/examples/creative-agent/subagents.yaml +28 -0
- muapi_cli-0.2.7/muapi/__init__.py +1 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/auth.py +181 -16
- muapi_cli-0.2.7/muapi/commands/init_cmd.py +62 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/mcp_server.py +25 -15
- muapi_cli-0.2.7/muapi/commands/open_cmd.py +47 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/config.py +17 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/main.py +13 -1
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/npm/package.json +1 -1
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/npm/scripts/install.js +2 -1
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/pyproject.toml +1 -1
- muapi_cli-0.2.5/.claude/settings.local.json +0 -24
- muapi_cli-0.2.5/muapi/__init__.py +0 -1
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/.github/workflows/publish-langchain.yml +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/.github/workflows/publish-npm.yml +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/.github/workflows/publish-pypi.yml +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/.gitignore +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/cli_entry.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/README.md +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/docs/providers-muapi.mdx +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/examples/deep_agents_demo.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/muapi_langchain/__init__.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/muapi_langchain/_client.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/muapi_langchain/_registry.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/muapi_langchain/callbacks.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/muapi_langchain/data/models.json +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/muapi_langchain/data/skills.json +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/muapi_langchain/loader.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/muapi_langchain/tools.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/pyproject.toml +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/integrations/langchain/scripts/refresh_registry.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/client.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/__init__.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/account.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/audio.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/config_cmd.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/docs.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/edit.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/enhance.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/image.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/keys.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/models.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/predict.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/run.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/upload.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/video.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/commands/workflow.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/dynamic_help.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/exitcodes.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/schema_introspect.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/muapi/utils.py +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/npm/README.md +0 -0
- {muapi_cli-0.2.5 → muapi_cli-0.2.7}/npm/bin/muapi +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: muapi-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Official CLI for muapi.ai — generative media at your fingertips
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -22,6 +22,11 @@ Official command-line interface for [muapi.ai](https://muapi.ai) — generate im
|
|
|
22
22
|
|
|
23
23
|
**Agent-first design** — every command works for both humans (colored output, tables) and AI agents (`--output-json`, `--jq` filtering, semantic exit codes, MCP server mode).
|
|
24
24
|
|
|
25
|
+
## Related Projects
|
|
26
|
+
|
|
27
|
+
- [Open-Generative-AI](https://github.com/Anil-matcha/Open-Generative-AI) — Browser-based GUI for the same models — no CLI required
|
|
28
|
+
- [Awesome-GPT-Image-2-API-Prompts](https://github.com/Anil-matcha/Awesome-GPT-Image-2-API-Prompts) — Curated prompt library to run via this CLI
|
|
29
|
+
|
|
25
30
|
## Install
|
|
26
31
|
|
|
27
32
|
```bash
|
|
@@ -4,6 +4,11 @@ Official command-line interface for [muapi.ai](https://muapi.ai) — generate im
|
|
|
4
4
|
|
|
5
5
|
**Agent-first design** — every command works for both humans (colored output, tables) and AI agents (`--output-json`, `--jq` filtering, semantic exit codes, MCP server mode).
|
|
6
6
|
|
|
7
|
+
## Related Projects
|
|
8
|
+
|
|
9
|
+
- [Open-Generative-AI](https://github.com/Anil-matcha/Open-Generative-AI) — Browser-based GUI for the same models — no CLI required
|
|
10
|
+
- [Awesome-GPT-Image-2-API-Prompts](https://github.com/Anil-matcha/Awesome-GPT-Image-2-API-Prompts) — Curated prompt library to run via this CLI
|
|
11
|
+
|
|
7
12
|
## Install
|
|
8
13
|
|
|
9
14
|
```bash
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# MuAPI Creative Agent
|
|
2
|
+
|
|
3
|
+
You are a creative-media AI agent powered by [muapi.ai](https://muapi.ai) — a unified API for 390+ generative media models. You can create images, videos, audio, 3D assets, and apply effects, enhancements, and transformations.
|
|
4
|
+
|
|
5
|
+
## Your Capabilities
|
|
6
|
+
|
|
7
|
+
| Kind | What you can do |
|
|
8
|
+
|------|----------------|
|
|
9
|
+
| **Image** | Generate from text (Flux, HiDream, Midjourney, GPT-4o, DALL-E, Ideogram…) |
|
|
10
|
+
| **Image edit** | Edit, inpaint, style transfer, background removal, upscale, colorize |
|
|
11
|
+
| **Video** | Text-to-video (Veo 3, Kling, Sora, Seedance, Runway, Pika…) |
|
|
12
|
+
| **Image-to-video** | Animate a still image |
|
|
13
|
+
| **Video edit** | Effects, lipsync, face swap, dance, dress change |
|
|
14
|
+
| **Audio** | Music generation (Suno), sound effects (MMAudio) |
|
|
15
|
+
| **3D** | Image or text to 3D model (Tripo3D, Meshy) |
|
|
16
|
+
|
|
17
|
+
## Tool Usage Guidelines
|
|
18
|
+
|
|
19
|
+
Always follow this decision tree:
|
|
20
|
+
|
|
21
|
+
1. **Don't know which model/skill fits?** → `muapi_select` first (free, instant)
|
|
22
|
+
2. **Single asset, clear prompt?** → `muapi_generate` directly
|
|
23
|
+
3. **Matches a named multi-step recipe?** → delegate to `creative-specialist` → `muapi_run_skill`
|
|
24
|
+
4. **Open-ended multi-asset brief?** → delegate to `creative-specialist` → `muapi_creative_agent`
|
|
25
|
+
|
|
26
|
+
## Quality Rules
|
|
27
|
+
|
|
28
|
+
- Always call `muapi_select` before generating if the user hasn't specified a model — it surfaces the best fit for cost and quality
|
|
29
|
+
- For edits and enhancements, pass `input_asset_url` to `muapi_generate`
|
|
30
|
+
- The `tier` parameter controls quality vs. speed: `"best"` for hero assets, `"balanced"` for iterating, `"fast"` for previews
|
|
31
|
+
- Report asset URLs to the user in every response — they're the deliverable
|
|
32
|
+
|
|
33
|
+
## What to Avoid
|
|
34
|
+
|
|
35
|
+
- Don't guess model names — use `muapi_select` to discover them
|
|
36
|
+
- Don't call `muapi_creative_agent` without `interrupt_on` approval — it can spend many credits
|
|
37
|
+
- Don't chain multiple `muapi_generate` calls when a skill covers the use case better
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# MuAPI Creative Agent
|
|
2
|
+
|
|
3
|
+
A generative-media Deep Agent powered by [muapi.ai](https://muapi.ai) — 390+ models for images, video, audio, 3D, and more, exposed as LangChain tools and wired into a Deep Agent with a planner/specialist split.
|
|
4
|
+
|
|
5
|
+
**This example demonstrates how to build a media-generation agent through three filesystem primitives:**
|
|
6
|
+
- **Memory** (`AGENTS.md`) — persistent context: available models, decision tree, quality rules
|
|
7
|
+
- **Skills** (`skills/*/SKILL.md`) — loaded-on-demand workflows for generation and named recipes
|
|
8
|
+
- **Subagents** (`subagents.yaml`) — a `creative-specialist` for heavy multi-step work
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Set API keys
|
|
14
|
+
export MUAPI_API_KEY="..." # Get one at muapi.ai
|
|
15
|
+
export ANTHROPIC_API_KEY="..."
|
|
16
|
+
|
|
17
|
+
# Run (uv installs dependencies automatically)
|
|
18
|
+
cd examples/creative-agent
|
|
19
|
+
uv run python creative_agent.py "Generate a cinematic product photo of sneakers"
|
|
20
|
+
|
|
21
|
+
# With a budget cap
|
|
22
|
+
uv run python creative_agent.py --budget 200 "Animate my product image into a 5s video"
|
|
23
|
+
|
|
24
|
+
# Multi-step recipe
|
|
25
|
+
uv run python creative_agent.py "Make a 3-shot Instagram carousel for SunFizz mango water"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
User brief
|
|
32
|
+
│
|
|
33
|
+
├─ AGENTS.md Loaded at startup — tells the agent what muapi can do
|
|
34
|
+
├─ skills/ Loaded on demand — step-by-step tool usage guides
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
Planner (Claude Sonnet)
|
|
38
|
+
├─ muapi_select Discover best model/skill for the brief (free)
|
|
39
|
+
├─ muapi_generate Single-shot generation: image / video / audio / 3D
|
|
40
|
+
│
|
|
41
|
+
└─ task ──────────────► creative-specialist subagent
|
|
42
|
+
├─ muapi_run_skill Named multi-step recipe
|
|
43
|
+
└─ muapi_creative_agent Open-ended brief (HITL-gated)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`MuapiCostCallback` tracks credit spend in real time and aborts the run if the budget cap is hit.
|
|
47
|
+
|
|
48
|
+
## Tool Capability Gradient
|
|
49
|
+
|
|
50
|
+
| Tool | Tier | What it does | Credits |
|
|
51
|
+
|------|------|-------------|---------|
|
|
52
|
+
| `muapi_select` | Planner | Rank models + skills for an intent | Free |
|
|
53
|
+
| `muapi_generate` | Planner | Generate one asset (any modality) | Per call |
|
|
54
|
+
| `muapi_run_skill` | Specialist | Run a named multi-step recipe | Per recipe |
|
|
55
|
+
| `muapi_creative_agent` | Specialist | Hand a full brief to muapi's planner (HITL-gated) | Variable |
|
|
56
|
+
|
|
57
|
+
## File Structure
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
creative-agent/
|
|
61
|
+
├── AGENTS.md # Persistent agent context
|
|
62
|
+
├── creative_agent.py # Main entry point
|
|
63
|
+
├── subagents.yaml # creative-specialist definition
|
|
64
|
+
├── pyproject.toml
|
|
65
|
+
└── skills/
|
|
66
|
+
├── generate-asset/SKILL.md # Single-asset generation workflow
|
|
67
|
+
└── run-skill/SKILL.md # Named recipe delegation workflow
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Environment Variables
|
|
71
|
+
|
|
72
|
+
| Variable | Required | Notes |
|
|
73
|
+
|----------|----------|-------|
|
|
74
|
+
| `MUAPI_API_KEY` | Yes | Get at [muapi.ai](https://muapi.ai) or run `muapi auth configure` |
|
|
75
|
+
| `ANTHROPIC_API_KEY` | Yes | Powers the planner (Claude Sonnet) |
|
|
76
|
+
|
|
77
|
+
## Related
|
|
78
|
+
|
|
79
|
+
- [muapi-langchain on PyPI](https://pypi.org/project/muapi-langchain/)
|
|
80
|
+
- [muapi.ai](https://muapi.ai) — API docs and model catalog
|
|
81
|
+
- [muapi CLI](https://github.com/SamurAIGPT/muapi-cli) — same auth, same client
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MuAPI Creative Agent
|
|
4
|
+
|
|
5
|
+
A generative-media Deep Agent configured through filesystem primitives:
|
|
6
|
+
- AGENTS.md — persistent context: capabilities, decision tree, quality rules
|
|
7
|
+
- skills/ — loaded-on-demand workflows (generate-asset, run-skill)
|
|
8
|
+
- subagents.yaml — creative-specialist subagent for heavy multi-step work
|
|
9
|
+
- MuapiCostCallback — real-time credit tracking with optional budget cap
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
export MUAPI_API_KEY="..." # or: muapi auth configure
|
|
13
|
+
export ANTHROPIC_API_KEY="..."
|
|
14
|
+
uv run python creative_agent.py "Generate a cinematic product photo of a sneaker"
|
|
15
|
+
uv run python creative_agent.py "Make a 3-shot Instagram carousel for SunFizz mango water"
|
|
16
|
+
uv run python creative_agent.py --budget 200 "Animate my logo"
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import yaml
|
|
26
|
+
from langchain_anthropic import ChatAnthropic
|
|
27
|
+
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
from rich.live import Live
|
|
30
|
+
from rich.markdown import Markdown
|
|
31
|
+
from rich.panel import Panel
|
|
32
|
+
from rich.spinner import Spinner
|
|
33
|
+
|
|
34
|
+
from deepagents import create_deep_agent
|
|
35
|
+
from muapi_langchain import (
|
|
36
|
+
PLANNER_TOOLS,
|
|
37
|
+
SPECIALIST_TOOLS,
|
|
38
|
+
MuapiCostCallback,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
EXAMPLE_DIR = Path(__file__).parent
|
|
42
|
+
console = Console()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_subagents(config_path: Path) -> list[dict]:
|
|
46
|
+
"""Load subagent definitions from YAML and wire up muapi specialist tools."""
|
|
47
|
+
tool_map = {t.name: t for t in SPECIALIST_TOOLS}
|
|
48
|
+
|
|
49
|
+
with open(config_path) as f:
|
|
50
|
+
config = yaml.safe_load(f)
|
|
51
|
+
|
|
52
|
+
subagents = []
|
|
53
|
+
for name, spec in config.items():
|
|
54
|
+
subagent: dict[str, Any] = {
|
|
55
|
+
"name": name,
|
|
56
|
+
"description": spec["description"],
|
|
57
|
+
"system_prompt": spec["system_prompt"],
|
|
58
|
+
}
|
|
59
|
+
if "model" in spec:
|
|
60
|
+
subagent["model"] = spec["model"]
|
|
61
|
+
if "tools" in spec:
|
|
62
|
+
subagent["tools"] = [tool_map[t] for t in spec["tools"] if t in tool_map]
|
|
63
|
+
subagents.append(subagent)
|
|
64
|
+
return subagents
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_creative_agent(budget_credits: int = 500):
|
|
68
|
+
cost_cb = MuapiCostCallback(
|
|
69
|
+
budget_credits=budget_credits,
|
|
70
|
+
on_event=lambda evt, payload: console.print(
|
|
71
|
+
f" [dim][{evt}] credits: {payload.get('credits', 0)} "
|
|
72
|
+
f"(total: {payload.get('running_total', 0)})[/]"
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
agent = create_deep_agent(
|
|
76
|
+
model=ChatAnthropic(model="claude-sonnet-4-6"),
|
|
77
|
+
memory=[str(EXAMPLE_DIR / "AGENTS.md")],
|
|
78
|
+
skills=[str(EXAMPLE_DIR / "skills/")],
|
|
79
|
+
tools=PLANNER_TOOLS,
|
|
80
|
+
subagents=load_subagents(EXAMPLE_DIR / "subagents.yaml"),
|
|
81
|
+
interrupt_on={
|
|
82
|
+
"muapi_creative_agent": {"allowed_decisions": ["approve", "edit", "reject"]},
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
return agent, cost_cb
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AgentDisplay:
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self.printed_count = 0
|
|
91
|
+
self._spinner = Spinner("dots", text="Thinking…")
|
|
92
|
+
|
|
93
|
+
def spinner(self, text: str = "Thinking…") -> Spinner:
|
|
94
|
+
self._spinner = Spinner("dots", text=text)
|
|
95
|
+
return self._spinner
|
|
96
|
+
|
|
97
|
+
def render(self, msg: Any) -> None:
|
|
98
|
+
if isinstance(msg, HumanMessage):
|
|
99
|
+
console.print(Panel(str(msg.content), title="You", border_style="blue"))
|
|
100
|
+
|
|
101
|
+
elif isinstance(msg, AIMessage):
|
|
102
|
+
content = msg.content
|
|
103
|
+
if isinstance(content, list):
|
|
104
|
+
content = "\n".join(
|
|
105
|
+
p.get("text", "") for p in content
|
|
106
|
+
if isinstance(p, dict) and p.get("type") == "text"
|
|
107
|
+
)
|
|
108
|
+
if content and content.strip():
|
|
109
|
+
console.print(Panel(Markdown(content), title="Agent", border_style="green"))
|
|
110
|
+
|
|
111
|
+
for tc in (msg.tool_calls or []):
|
|
112
|
+
name = tc.get("name", "")
|
|
113
|
+
args = tc.get("args", {})
|
|
114
|
+
if name == "muapi_select":
|
|
115
|
+
console.print(f" [bold cyan]→ Discovering models for:[/] {args.get('intent', '')[:60]}")
|
|
116
|
+
self.spinner("Discovering models…")
|
|
117
|
+
elif name == "muapi_generate":
|
|
118
|
+
console.print(f" [bold magenta]→ Generating {args.get('kind', 'asset')}:[/] {args.get('prompt', '')[:60]}")
|
|
119
|
+
self.spinner("Generating…")
|
|
120
|
+
elif name == "task":
|
|
121
|
+
desc = args.get("description", "")
|
|
122
|
+
console.print(f" [bold yellow]→ Delegating:[/] {desc[:70]}")
|
|
123
|
+
self.spinner("Delegating to specialist…")
|
|
124
|
+
|
|
125
|
+
elif isinstance(msg, ToolMessage):
|
|
126
|
+
name = getattr(msg, "name", "")
|
|
127
|
+
content = str(msg.content)
|
|
128
|
+
if name == "muapi_generate":
|
|
129
|
+
if '"ok": true' in content or '"url":' in content:
|
|
130
|
+
import json
|
|
131
|
+
try:
|
|
132
|
+
data = json.loads(content)
|
|
133
|
+
url = data.get("url", "")
|
|
134
|
+
console.print(f" [green]✓ Generated:[/] {url}")
|
|
135
|
+
except Exception:
|
|
136
|
+
console.print(f" [green]✓ Generated[/]")
|
|
137
|
+
else:
|
|
138
|
+
console.print(f" [red]✗ Generation failed[/]")
|
|
139
|
+
elif name == "muapi_select":
|
|
140
|
+
console.print(f" [green]✓ Models discovered[/]")
|
|
141
|
+
elif name == "task":
|
|
142
|
+
console.print(f" [green]✓ Specialist done[/]")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def run(brief: str, budget_credits: int = 500) -> None:
|
|
146
|
+
agent, cost_cb = create_creative_agent(budget_credits)
|
|
147
|
+
display = AgentDisplay()
|
|
148
|
+
|
|
149
|
+
console.print()
|
|
150
|
+
console.print("[bold blue]MuAPI Creative Agent[/]")
|
|
151
|
+
console.print(f"[dim]Brief: {brief}[/]")
|
|
152
|
+
console.print(f"[dim]Budget: {budget_credits} credits[/]")
|
|
153
|
+
console.print()
|
|
154
|
+
|
|
155
|
+
config = {"configurable": {"thread_id": "creative-agent-demo"}, "callbacks": [cost_cb]}
|
|
156
|
+
|
|
157
|
+
with Live(display.spinner(), console=console, refresh_per_second=10, transient=True) as live:
|
|
158
|
+
async for chunk in agent.astream(
|
|
159
|
+
{"messages": [("user", brief)]},
|
|
160
|
+
config=config,
|
|
161
|
+
stream_mode="values",
|
|
162
|
+
):
|
|
163
|
+
if "messages" not in chunk:
|
|
164
|
+
continue
|
|
165
|
+
messages = chunk["messages"]
|
|
166
|
+
if len(messages) <= display.printed_count:
|
|
167
|
+
continue
|
|
168
|
+
live.stop()
|
|
169
|
+
for msg in messages[display.printed_count:]:
|
|
170
|
+
display.render(msg)
|
|
171
|
+
display.printed_count = len(messages)
|
|
172
|
+
live.start()
|
|
173
|
+
live.update(display.spinner())
|
|
174
|
+
|
|
175
|
+
summary = cost_cb.summary()
|
|
176
|
+
console.print()
|
|
177
|
+
console.print(
|
|
178
|
+
f"[bold green]✓ Done[/] — "
|
|
179
|
+
f"[dim]{summary['total_credits']} credits across {summary['calls']} calls[/]"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main() -> None:
|
|
184
|
+
import argparse
|
|
185
|
+
|
|
186
|
+
parser = argparse.ArgumentParser(description="MuAPI Creative Agent")
|
|
187
|
+
parser.add_argument("brief", nargs="*", help="Creative brief (or omit for demo)")
|
|
188
|
+
parser.add_argument("--budget", type=int, default=500, help="Credit budget cap (default 500)")
|
|
189
|
+
args = parser.parse_args()
|
|
190
|
+
|
|
191
|
+
brief = " ".join(args.brief) if args.brief else (
|
|
192
|
+
"Generate a cinematic product photo of a pair of minimalist white sneakers "
|
|
193
|
+
"on a wet cobblestone street at night, neon reflections"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
asyncio.run(run(brief, budget_credits=args.budget))
|
|
198
|
+
except KeyboardInterrupt:
|
|
199
|
+
console.print("\n[yellow]Interrupted[/]")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "muapi-creative-agent"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A generative-media Deep Agent powered by muapi.ai"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"deepagents>=0.3.5",
|
|
8
|
+
"muapi-langchain>=0.1.0",
|
|
9
|
+
"langchain-anthropic>=0.3.0",
|
|
10
|
+
"pyyaml>=6.0.0",
|
|
11
|
+
"rich>=13.0.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = []
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: generate-asset
|
|
3
|
+
description: Generate a single media asset — image, video, audio, or 3D. Use when the user wants one output from a clear prompt. Calls muapi_select to pick the best model, then muapi_generate.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Generate Asset
|
|
7
|
+
|
|
8
|
+
Use this skill for single-asset generation tasks.
|
|
9
|
+
|
|
10
|
+
## Step 1 — Discover
|
|
11
|
+
|
|
12
|
+
Call `muapi_select` with the user's intent and kind:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
muapi_select(intent="<user's prompt>", kind="<image|video|audio|3d>", tier="<best|balanced|fast>", limit=3)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Pick the top-ranked model from the result.
|
|
19
|
+
|
|
20
|
+
## Step 2 — Generate
|
|
21
|
+
|
|
22
|
+
Call `muapi_generate` with the chosen model:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
muapi_generate(
|
|
26
|
+
prompt="<refined prompt>",
|
|
27
|
+
kind="<kind>",
|
|
28
|
+
model="<name from muapi_select>",
|
|
29
|
+
tier="<best|balanced|fast>",
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For edits, enhancements, image-to-video, or lipsync — pass `input_asset_url` too.
|
|
34
|
+
|
|
35
|
+
## Step 3 — Return
|
|
36
|
+
|
|
37
|
+
Report the asset URL to the user. Include: model used, kind, and any relevant parameters.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-skill
|
|
3
|
+
description: Run a named muapi multi-step recipe (UGC ad, storyboard, brand kit, product video, social carousel…). Use when the brief matches a known workflow. Delegates to the creative-specialist subagent.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Run Named Skill
|
|
7
|
+
|
|
8
|
+
Use this skill when the user's brief matches a known multi-step recipe.
|
|
9
|
+
|
|
10
|
+
## Step 1 — Discover skills
|
|
11
|
+
|
|
12
|
+
Call `muapi_select` with the user's intent. The result includes a `skills` list — check if any skill name matches the brief:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
muapi_select(intent="<user's brief>", limit=5)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If a skill name matches (e.g., "ugc-ads-workflow", "storyboard", "brand-kit"), use it.
|
|
19
|
+
|
|
20
|
+
## Step 2 — Delegate to creative-specialist
|
|
21
|
+
|
|
22
|
+
Delegate to the `creative-specialist` subagent with:
|
|
23
|
+
- The matching skill name
|
|
24
|
+
- The user's inputs (from the skill's declared `inputs` schema)
|
|
25
|
+
|
|
26
|
+
The specialist will call `muapi_run_skill(skill_name=..., inputs={...})`.
|
|
27
|
+
|
|
28
|
+
## Step 3 — Return
|
|
29
|
+
|
|
30
|
+
Collect all asset URLs from the specialist and return them with a brief summary of what was created.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Subagent definitions for the muapi creative agent
|
|
2
|
+
|
|
3
|
+
creative-specialist:
|
|
4
|
+
description: >
|
|
5
|
+
Handles heavy muapi workflows: named multi-step skills (UGC ads, storyboards,
|
|
6
|
+
product videos, brand kits, social carousels) and open-ended multi-asset briefs.
|
|
7
|
+
Delegate here when the brief needs more than one generation call or matches a
|
|
8
|
+
named recipe discovered via muapi_select.
|
|
9
|
+
system_prompt: |
|
|
10
|
+
You are a muapi creative specialist with access to muapi_run_skill and muapi_creative_agent.
|
|
11
|
+
|
|
12
|
+
## Decision rule
|
|
13
|
+
- If the brief matches a named skill from muapi_select → use muapi_run_skill
|
|
14
|
+
- If it's an open-ended multi-asset brief with no matching skill → use muapi_creative_agent
|
|
15
|
+
|
|
16
|
+
## muapi_run_skill usage
|
|
17
|
+
muapi_run_skill(skill_name="<name>", inputs={"key": "value", ...})
|
|
18
|
+
Inputs must match the skill's declared schema (get it from muapi_select).
|
|
19
|
+
|
|
20
|
+
## muapi_creative_agent usage
|
|
21
|
+
muapi_creative_agent(brief="<full description>", budget_credits=<int>)
|
|
22
|
+
Use only when explicitly approved by the user (this can spend significant credits).
|
|
23
|
+
|
|
24
|
+
## Output
|
|
25
|
+
Always return all asset URLs with a one-line description of each.
|
|
26
|
+
tools:
|
|
27
|
+
- muapi_run_skill
|
|
28
|
+
- muapi_creative_agent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.7"
|
|
@@ -1,16 +1,121 @@
|
|
|
1
1
|
"""muapi auth — configure API key and inspect identity."""
|
|
2
|
-
import
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import webbrowser
|
|
7
|
+
|
|
3
8
|
import httpx
|
|
4
|
-
|
|
9
|
+
import typer
|
|
10
|
+
from rich.prompt import Confirm, Prompt
|
|
5
11
|
|
|
6
|
-
from ..config import delete_api_key, get_api_key,
|
|
12
|
+
from ..config import BASE_URL, _CONFIG_FILE, delete_api_key, get_api_key, get_key_info, save_api_key
|
|
7
13
|
from .. import exitcodes
|
|
8
14
|
from ..utils import console, error_exit, out
|
|
9
15
|
|
|
10
16
|
app = typer.Typer(help="Manage authentication and API key.")
|
|
11
17
|
|
|
12
|
-
# Auth endpoints live at the root host, not under /api/v1
|
|
13
18
|
_AUTH_BASE = BASE_URL.replace("/api/v1", "")
|
|
19
|
+
_ACCESS_KEYS_URL = "https://muapi.ai/access-keys"
|
|
20
|
+
|
|
21
|
+
LINKS = {
|
|
22
|
+
"dashboard": "https://muapi.ai/dashboard",
|
|
23
|
+
"access-keys": _ACCESS_KEYS_URL,
|
|
24
|
+
"models": "https://muapi.ai/models",
|
|
25
|
+
"docs": "https://muapi.ai/docs",
|
|
26
|
+
"pricing": "https://muapi.ai/pricing",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _mask(key: str) -> str:
|
|
31
|
+
if len(key) < 12:
|
|
32
|
+
return "••••"
|
|
33
|
+
return key[:8] + "…" + key[-4:]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _looks_like_key(s: str) -> bool:
|
|
37
|
+
s = s.strip()
|
|
38
|
+
return bool(re.match(r'^[A-Za-z0-9_\-]{20,}$', s) and '\n' not in s)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _read_clipboard() -> str | None:
|
|
42
|
+
try:
|
|
43
|
+
if sys.platform == "darwin":
|
|
44
|
+
r = subprocess.run(["pbpaste"], capture_output=True, text=True, timeout=2)
|
|
45
|
+
return r.stdout.strip() or None
|
|
46
|
+
if sys.platform.startswith("linux"):
|
|
47
|
+
for cmd in (["xclip", "-o"], ["xsel", "--clipboard", "--output"]):
|
|
48
|
+
try:
|
|
49
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=2)
|
|
50
|
+
if r.returncode == 0:
|
|
51
|
+
return r.stdout.strip() or None
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
continue
|
|
54
|
+
if sys.platform == "win32":
|
|
55
|
+
r = subprocess.run(
|
|
56
|
+
["powershell", "-command", "Get-Clipboard"],
|
|
57
|
+
capture_output=True, text=True, timeout=2,
|
|
58
|
+
)
|
|
59
|
+
return r.stdout.strip() or None
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _validate_key(api_key: str) -> tuple[bool, str]:
|
|
66
|
+
"""Validate key against the live API. Returns (ok, error_msg)."""
|
|
67
|
+
try:
|
|
68
|
+
resp = httpx.get(
|
|
69
|
+
f"{BASE_URL}/account/balance",
|
|
70
|
+
headers={"x-api-key": api_key},
|
|
71
|
+
timeout=15.0,
|
|
72
|
+
)
|
|
73
|
+
if resp.status_code in (401, 403):
|
|
74
|
+
return False, "API rejected the key (401/403)."
|
|
75
|
+
if resp.status_code >= 400:
|
|
76
|
+
return False, f"API returned {resp.status_code}."
|
|
77
|
+
return True, ""
|
|
78
|
+
except httpx.RequestError as e:
|
|
79
|
+
return False, f"Could not reach {BASE_URL}: {e}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _find_project_config() -> str | None:
|
|
83
|
+
"""Walk up from CWD looking for muapi.json."""
|
|
84
|
+
from pathlib import Path
|
|
85
|
+
d = Path.cwd()
|
|
86
|
+
while True:
|
|
87
|
+
candidate = d / "muapi.json"
|
|
88
|
+
if candidate.exists():
|
|
89
|
+
return str(candidate)
|
|
90
|
+
parent = d.parent
|
|
91
|
+
if parent == d:
|
|
92
|
+
return None
|
|
93
|
+
d = parent
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _do_save(api_key: str) -> None:
|
|
97
|
+
with console.status("[dim]Validating with api.muapi.ai…[/dim]"):
|
|
98
|
+
ok, reason = _validate_key(api_key)
|
|
99
|
+
|
|
100
|
+
if not ok:
|
|
101
|
+
console.print(f"[bold red]✖[/bold red] {reason}")
|
|
102
|
+
console.print()
|
|
103
|
+
console.print(f"[dim] Double-check at [/dim][cyan]{_ACCESS_KEYS_URL}[/cyan]")
|
|
104
|
+
console.print("[dim] Or set [/dim][cyan]MUAPI_API_KEY[/cyan][dim] in your shell and skip this step.[/dim]\n")
|
|
105
|
+
raise typer.Exit(exitcodes.AUTH_ERROR)
|
|
106
|
+
|
|
107
|
+
location = save_api_key(api_key)
|
|
108
|
+
location_display = "OS keychain" if location == "keychain" else str(_CONFIG_FILE)
|
|
109
|
+
console.print("[bold green]✔[/bold green] Signed in.")
|
|
110
|
+
console.print()
|
|
111
|
+
console.print(f" [dim]Key: [/dim][green]{_mask(api_key)}[/green]")
|
|
112
|
+
console.print(f" [dim]Stored: [/dim][cyan]{location_display}[/cyan]")
|
|
113
|
+
console.print()
|
|
114
|
+
console.print("[bold]Try it:[/bold]")
|
|
115
|
+
console.print(" [cyan]muapi account balance[/cyan]")
|
|
116
|
+
console.print(" [cyan]muapi image generate -p \"a cyberpunk skyline at golden hour\"[/cyan]")
|
|
117
|
+
console.print(" [cyan]muapi video generate -p \"drone shot over snowy peaks\" --model kling-master[/cyan]")
|
|
118
|
+
console.print()
|
|
14
119
|
|
|
15
120
|
|
|
16
121
|
@app.command("login")
|
|
@@ -165,25 +270,85 @@ def reset_password(
|
|
|
165
270
|
|
|
166
271
|
@app.command("configure")
|
|
167
272
|
def configure(
|
|
168
|
-
api_key: str = typer.Option(None, "--api-key", "-k", help="API key (
|
|
273
|
+
api_key: str = typer.Option(None, "--api-key", "-k", help="API key (skips all prompts)"),
|
|
274
|
+
no_browser: bool = typer.Option(False, "--no-browser", help="Skip opening the access-keys page"),
|
|
169
275
|
):
|
|
170
|
-
"""Save your muapi API key
|
|
276
|
+
"""Save your muapi API key — opens browser, detects clipboard, validates before saving."""
|
|
277
|
+
if api_key:
|
|
278
|
+
_do_save(api_key.strip())
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
console.print()
|
|
282
|
+
console.print("[bold magenta] Welcome to muapi.[/bold magenta]")
|
|
283
|
+
console.print("[dim] Sign in once and you're set on this machine.[/dim]\n")
|
|
284
|
+
|
|
285
|
+
if not no_browser:
|
|
286
|
+
console.print(f"[bold] 1.[/bold] Opening [cyan]{_ACCESS_KEYS_URL}[/cyan]")
|
|
287
|
+
try:
|
|
288
|
+
webbrowser.open(_ACCESS_KEYS_URL)
|
|
289
|
+
except Exception:
|
|
290
|
+
console.print("[dim] (browser launch failed — open the link manually)[/dim]")
|
|
291
|
+
console.print("[bold] 2.[/bold] Copy your API key")
|
|
292
|
+
console.print("[bold] 3.[/bold] Paste below — we'll validate it automatically\n")
|
|
293
|
+
|
|
294
|
+
# Clipboard detection
|
|
295
|
+
detected: str | None = None
|
|
296
|
+
clip = _read_clipboard()
|
|
297
|
+
if clip and _looks_like_key(clip):
|
|
298
|
+
detected = clip
|
|
299
|
+
|
|
300
|
+
if detected:
|
|
301
|
+
use_it = Confirm.ask(
|
|
302
|
+
f" Detected a key on your clipboard ({_mask(detected)}). Use it?",
|
|
303
|
+
default=True,
|
|
304
|
+
console=console,
|
|
305
|
+
)
|
|
306
|
+
if use_it:
|
|
307
|
+
api_key = detected
|
|
308
|
+
|
|
171
309
|
if not api_key:
|
|
172
|
-
api_key = Prompt.ask("[bold]
|
|
310
|
+
api_key = Prompt.ask("[bold] Paste your API key[/bold]", password=True, console=console)
|
|
311
|
+
api_key = api_key.strip()
|
|
312
|
+
|
|
173
313
|
if not api_key:
|
|
174
|
-
error_exit("No API key provided.")
|
|
175
|
-
|
|
176
|
-
|
|
314
|
+
error_exit("No API key provided.", exitcodes.AUTH_ERROR)
|
|
315
|
+
|
|
316
|
+
_do_save(api_key)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@app.command("status")
|
|
320
|
+
def status():
|
|
321
|
+
"""Show the active API key, config location, base URL, and quick links."""
|
|
322
|
+
key, source = get_key_info()
|
|
323
|
+
|
|
324
|
+
console.print()
|
|
325
|
+
console.print("[bold]muapi CLI status[/bold]")
|
|
326
|
+
if key:
|
|
327
|
+
console.print(f" [dim]API key: [/dim][green]{_mask(key)}[/green]")
|
|
328
|
+
else:
|
|
329
|
+
console.print(f" [dim]API key: [/dim][red]not set — run [bold]muapi auth configure[/bold][/red]")
|
|
330
|
+
console.print(f" [dim]Source: [/dim][cyan]{source}[/cyan]")
|
|
331
|
+
console.print(f" [dim]Base URL: [/dim][cyan]{BASE_URL}[/cyan]")
|
|
332
|
+
console.print(f" [dim]Config: [/dim][cyan]{_CONFIG_FILE}[/cyan]")
|
|
333
|
+
|
|
334
|
+
project_file = _find_project_config()
|
|
335
|
+
if project_file:
|
|
336
|
+
console.print(f" [dim]Project: [/dim][cyan]{project_file}[/cyan] [dim](muapi.json detected)[/dim]")
|
|
337
|
+
|
|
338
|
+
console.print()
|
|
339
|
+
console.print("[bold]Useful links[/bold]")
|
|
340
|
+
width = max(len(k) for k in LINKS)
|
|
341
|
+
for name, url in LINKS.items():
|
|
342
|
+
console.print(f" [dim]{name.ljust(width)}[/dim] [cyan]{url}[/cyan]")
|
|
343
|
+
console.print()
|
|
344
|
+
console.print("[dim]Jump in your browser: [/dim][cyan]muapi open <target>[/cyan]")
|
|
345
|
+
console.print()
|
|
177
346
|
|
|
178
347
|
|
|
179
348
|
@app.command("whoami")
|
|
180
349
|
def whoami():
|
|
181
|
-
"""
|
|
182
|
-
|
|
183
|
-
if not key:
|
|
184
|
-
error_exit("No API key configured. Run: muapi auth configure", exitcodes.AUTH_ERROR)
|
|
185
|
-
masked = key[:8] + "..." + key[-4:]
|
|
186
|
-
out.print(f"API key: [bold]{masked}[/bold]")
|
|
350
|
+
"""Alias for [bold]muapi auth status[/bold]."""
|
|
351
|
+
status()
|
|
187
352
|
|
|
188
353
|
|
|
189
354
|
@app.command("logout")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""muapi init — create a muapi.json project config in the current directory."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..utils import console, error_exit
|
|
8
|
+
|
|
9
|
+
_PROJECT_FILE = "muapi.json"
|
|
10
|
+
_SCHEMA_URL = "https://muapi.ai/schema/cli.json"
|
|
11
|
+
|
|
12
|
+
_DEFAULT_CONFIG = {
|
|
13
|
+
"$schema": _SCHEMA_URL,
|
|
14
|
+
"defaultModel": "flux-dev-image",
|
|
15
|
+
"outputDir": "muapi-output",
|
|
16
|
+
"aliases": {},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def init(
|
|
21
|
+
yes: bool = typer.Option(False, "-y", "--yes", help="Skip prompts and write defaults"),
|
|
22
|
+
force: bool = typer.Option(False, "-f", "--force", help="Overwrite an existing muapi.json"),
|
|
23
|
+
):
|
|
24
|
+
"""Create a muapi.json project config with a defaultModel and alias stubs.
|
|
25
|
+
|
|
26
|
+
\b
|
|
27
|
+
Examples:
|
|
28
|
+
muapi init # interactive
|
|
29
|
+
muapi init -y # write defaults silently
|
|
30
|
+
muapi init -y -f # overwrite existing
|
|
31
|
+
"""
|
|
32
|
+
target = Path.cwd() / _PROJECT_FILE
|
|
33
|
+
|
|
34
|
+
if target.exists() and not force:
|
|
35
|
+
error_exit(
|
|
36
|
+
f"{_PROJECT_FILE} already exists. Use --force to overwrite.",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
config = dict(_DEFAULT_CONFIG)
|
|
40
|
+
|
|
41
|
+
if not yes:
|
|
42
|
+
from rich.prompt import Prompt
|
|
43
|
+
default_model = Prompt.ask(
|
|
44
|
+
"Default model",
|
|
45
|
+
default=config["defaultModel"],
|
|
46
|
+
console=console,
|
|
47
|
+
)
|
|
48
|
+
output_dir = Prompt.ask(
|
|
49
|
+
"Output directory",
|
|
50
|
+
default=config["outputDir"],
|
|
51
|
+
console=console,
|
|
52
|
+
)
|
|
53
|
+
config["defaultModel"] = default_model
|
|
54
|
+
config["outputDir"] = output_dir
|
|
55
|
+
|
|
56
|
+
target.write_text(json.dumps(config, indent=2) + "\n")
|
|
57
|
+
console.print(f"[green]Wrote {_PROJECT_FILE}[/green]")
|
|
58
|
+
console.print()
|
|
59
|
+
console.print("Now try:")
|
|
60
|
+
console.print(f" [cyan]muapi run -p \"a serene mountain lake at sunrise\"[/cyan]")
|
|
61
|
+
console.print(f" Add aliases by editing [bold]{_PROJECT_FILE}[/bold]")
|
|
62
|
+
console.print()
|
|
@@ -15,7 +15,7 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
import typer
|
|
17
17
|
|
|
18
|
-
from .. import client as api_client
|
|
18
|
+
from .. import __version__, client as api_client
|
|
19
19
|
from ..config import get_api_key
|
|
20
20
|
|
|
21
21
|
app = typer.Typer(help="Run muapi as an MCP server for AI agent integration.")
|
|
@@ -360,15 +360,20 @@ TOOLS = [
|
|
|
360
360
|
"annotations": {"readOnlyHint": True, "idempotentHint": True},
|
|
361
361
|
"inputSchema": {"type": "object", "properties": {}},
|
|
362
362
|
"outputSchema": {
|
|
363
|
-
"type": "
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
|
|
367
|
-
"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
363
|
+
"type": "object",
|
|
364
|
+
"properties": {
|
|
365
|
+
"keys": {
|
|
366
|
+
"type": "array",
|
|
367
|
+
"items": {
|
|
368
|
+
"type": "object",
|
|
369
|
+
"properties": {
|
|
370
|
+
"id": {"type": "integer"},
|
|
371
|
+
"name": {"type": "string"},
|
|
372
|
+
"is_active": {"type": "boolean"},
|
|
373
|
+
"created_at": {"type": "string"},
|
|
374
|
+
"last_used_at": {"type": "string"},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
372
377
|
},
|
|
373
378
|
},
|
|
374
379
|
},
|
|
@@ -418,7 +423,12 @@ TOOLS = [
|
|
|
418
423
|
"endpoint": None,
|
|
419
424
|
"annotations": {"readOnlyHint": True, "idempotentHint": True},
|
|
420
425
|
"inputSchema": {"type": "object", "properties": {}},
|
|
421
|
-
"outputSchema": {
|
|
426
|
+
"outputSchema": {
|
|
427
|
+
"type": "object",
|
|
428
|
+
"properties": {
|
|
429
|
+
"workflows": {"type": "array"},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
422
432
|
},
|
|
423
433
|
{
|
|
424
434
|
"name": "muapi_workflow_create",
|
|
@@ -658,7 +668,7 @@ def _dispatch(tool_name: str, args: dict) -> dict:
|
|
|
658
668
|
resp = _httpx.get(f"{wf_base}/get-workflow-defs", headers={"x-api-key": key}, timeout=30.0)
|
|
659
669
|
if resp.status_code >= 400:
|
|
660
670
|
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
661
|
-
return resp.json()
|
|
671
|
+
return {"workflows": resp.json()}
|
|
662
672
|
|
|
663
673
|
if tool_name == "muapi_workflow_create":
|
|
664
674
|
from ..config import BASE_URL, get_api_key
|
|
@@ -732,7 +742,7 @@ def _dispatch(tool_name: str, args: dict) -> dict:
|
|
|
732
742
|
resp = _httpx.get(f"{BASE_URL}/keys", headers={"x-api-key": key}, timeout=30.0)
|
|
733
743
|
if resp.status_code >= 400:
|
|
734
744
|
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
735
|
-
return resp.json()
|
|
745
|
+
return {"keys": resp.json()}
|
|
736
746
|
|
|
737
747
|
if tool_name == "muapi_keys_create":
|
|
738
748
|
from ..config import BASE_URL, get_api_key
|
|
@@ -821,7 +831,7 @@ def _handle_request(request: dict) -> str:
|
|
|
821
831
|
return _mcp_response(req_id, {
|
|
822
832
|
"protocolVersion": "2025-06-18",
|
|
823
833
|
"capabilities": {"tools": {"listChanged": False}},
|
|
824
|
-
"serverInfo": {"name": "muapi", "version":
|
|
834
|
+
"serverInfo": {"name": "muapi", "version": __version__},
|
|
825
835
|
})
|
|
826
836
|
|
|
827
837
|
if method == "tools/list":
|
|
@@ -884,7 +894,7 @@ def serve(
|
|
|
884
894
|
)
|
|
885
895
|
sys.exit(3)
|
|
886
896
|
|
|
887
|
-
sys.stderr.write(json.dumps({"status": "muapi MCP server ready", "tools": len(TOOLS), "version":
|
|
897
|
+
sys.stderr.write(json.dumps({"status": "muapi MCP server ready", "tools": len(TOOLS), "version": __version__}) + "\n")
|
|
888
898
|
sys.stderr.flush()
|
|
889
899
|
|
|
890
900
|
for line in sys.stdin:
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""muapi open — open muapi.ai pages in your browser."""
|
|
2
|
+
import webbrowser
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..utils import console, error_exit
|
|
8
|
+
|
|
9
|
+
_TARGETS: dict[str, str] = {
|
|
10
|
+
"dashboard": "https://muapi.ai/dashboard",
|
|
11
|
+
"access-keys": "https://muapi.ai/access-keys",
|
|
12
|
+
"models": "https://muapi.ai/models",
|
|
13
|
+
"docs": "https://muapi.ai/docs",
|
|
14
|
+
"pricing": "https://muapi.ai/pricing",
|
|
15
|
+
"api": "https://api.muapi.ai/docs",
|
|
16
|
+
"discord": "https://discord.gg/muapi",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def open_page(
|
|
21
|
+
target: Optional[str] = typer.Argument(
|
|
22
|
+
None,
|
|
23
|
+
help="Page to open: " + ", ".join(_TARGETS.keys()),
|
|
24
|
+
),
|
|
25
|
+
):
|
|
26
|
+
"""Open a muapi.ai page in your browser.
|
|
27
|
+
|
|
28
|
+
\b
|
|
29
|
+
Examples:
|
|
30
|
+
muapi open # opens dashboard
|
|
31
|
+
muapi open access-keys # key management
|
|
32
|
+
muapi open models # model catalog
|
|
33
|
+
muapi open docs # API docs
|
|
34
|
+
"""
|
|
35
|
+
if not target:
|
|
36
|
+
target = "dashboard"
|
|
37
|
+
|
|
38
|
+
url = _TARGETS.get(target.lower())
|
|
39
|
+
if not url:
|
|
40
|
+
valid = ", ".join(_TARGETS.keys())
|
|
41
|
+
error_exit(f"Unknown target '{target}'. Valid: {valid}")
|
|
42
|
+
|
|
43
|
+
console.print(f"Opening [cyan]{url}[/cyan]")
|
|
44
|
+
try:
|
|
45
|
+
webbrowser.open(url)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
error_exit(f"Could not open browser: {e}")
|
|
@@ -96,6 +96,23 @@ def get_all_settings() -> dict:
|
|
|
96
96
|
return {}
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
def get_key_info() -> tuple[Optional[str], str]:
|
|
100
|
+
"""Return (api_key, source_description) for display in status/whoami."""
|
|
101
|
+
if key := os.environ.get("MUAPI_API_KEY"):
|
|
102
|
+
return key, "env:MUAPI_API_KEY"
|
|
103
|
+
ok, val = _try_keyring()
|
|
104
|
+
if ok and val:
|
|
105
|
+
return val, "keychain"
|
|
106
|
+
if _CONFIG_FILE.exists():
|
|
107
|
+
try:
|
|
108
|
+
data = json.loads(_CONFIG_FILE.read_text())
|
|
109
|
+
if key := data.get("api_key"):
|
|
110
|
+
return key, f"file:{_CONFIG_FILE}"
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
return None, "not set"
|
|
114
|
+
|
|
115
|
+
|
|
99
116
|
def delete_api_key() -> None:
|
|
100
117
|
ok, _ = _try_keyring()
|
|
101
118
|
if ok:
|
|
@@ -5,7 +5,7 @@ import typer
|
|
|
5
5
|
from rich import print as rprint
|
|
6
6
|
|
|
7
7
|
from . import __version__
|
|
8
|
-
from .commands import auth, account, audio, config_cmd, docs, edit, enhance, image, keys, models, predict, run, upload, video, workflow
|
|
8
|
+
from .commands import auth, account, audio, config_cmd, docs, edit, enhance, image, init_cmd, keys, models, open_cmd, predict, run, upload, video, workflow
|
|
9
9
|
from .commands import mcp_server
|
|
10
10
|
from .dynamic_help import maybe_handle_run_help
|
|
11
11
|
|
|
@@ -44,6 +44,18 @@ app.add_typer(config_cmd.app, name="config", help="Get and set persistent CLI
|
|
|
44
44
|
app.add_typer(docs.app, name="docs", help="Access the muapi.ai API documentation.")
|
|
45
45
|
app.add_typer(mcp_server.app, name="mcp", help="Run as an MCP server for AI agent integration.")
|
|
46
46
|
|
|
47
|
+
app.command(
|
|
48
|
+
"init",
|
|
49
|
+
help="Create a muapi.json project config with defaultModel and alias stubs.",
|
|
50
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
51
|
+
)(init_cmd.init)
|
|
52
|
+
|
|
53
|
+
app.command(
|
|
54
|
+
"open",
|
|
55
|
+
help="Open a muapi.ai page in your browser (dashboard, models, docs, access-keys…).",
|
|
56
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
57
|
+
)(open_cmd.open_page)
|
|
58
|
+
|
|
47
59
|
|
|
48
60
|
@app.command("version")
|
|
49
61
|
def version(
|
|
@@ -22,7 +22,8 @@ const NAME_MAP = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const key = `${process.platform}-${process.arch}`;
|
|
25
|
-
|
|
25
|
+
// On Apple Silicon, Node may report x64 when running under Rosetta — fall back to arm64.
|
|
26
|
+
const binName = NAME_MAP[key] || (process.platform === "darwin" ? NAME_MAP["darwin-arm64"] : undefined);
|
|
26
27
|
|
|
27
28
|
if (!binName) {
|
|
28
29
|
console.warn(`[muapi] Unsupported platform: ${key}. Install via pip: pip install muapi-cli`);
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(gh issue *)",
|
|
5
|
-
"Bash(curl -sI \"https://github.com/Anil-matcha/muapiapp/releases/download/v0.2.3/muapi-linux-x86_64\")",
|
|
6
|
-
"Bash(curl -sI \"https://github.com/SamurAIGPT/muapi-cli/releases/download/v0.2.3/muapi-linux-x86_64\")",
|
|
7
|
-
"Bash(gh release *)",
|
|
8
|
-
"Bash(npm view *)",
|
|
9
|
-
"Bash(git -C /Users/anilchandranaidumatcha/Downloads/mu_workflow/muapi-cli status)",
|
|
10
|
-
"Bash(git -C /Users/anilchandranaidumatcha/Downloads/mu_workflow/muapi-cli remote -v)",
|
|
11
|
-
"Bash(gh auth *)",
|
|
12
|
-
"Bash(npm whoami *)",
|
|
13
|
-
"Bash(git *)",
|
|
14
|
-
"Bash(tar -xzf muapi-cli-0.2.5.tgz)",
|
|
15
|
-
"Bash(tar -xzf muapi-cli-0.2.3.tgz)",
|
|
16
|
-
"Bash(tar -xzf muapi-cli-0.2.4.tgz)",
|
|
17
|
-
"Bash(rm -rf /tmp/muapi-022-check)",
|
|
18
|
-
"Bash(mkdir /tmp/muapi-022-check)",
|
|
19
|
-
"Read(//tmp/**)",
|
|
20
|
-
"Bash(npm pack *)",
|
|
21
|
-
"Bash(tar -xzf muapi-cli-0.2.2.tgz)"
|
|
22
|
-
]
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.0"
|
|
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
|