diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1159052..5984962 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -2,7 +2,7 @@ "name": "aiturk-ide", "productName": "AITURK IDE", "private": true, - "version": "1.0.0-beta.12", + "version": "1.0.0-beta.13", "description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.", "author": "AITURK / TurkServis", "repository": { diff --git a/hermes_cli/aiturk_media.py b/hermes_cli/aiturk_media.py deleted file mode 100644 index be02567..0000000 --- a/hermes_cli/aiturk_media.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Connect the AITURK distribution's media tools to its existing member key.""" - - -def configure_existing_media(cfg: dict) -> bool: - """Migrate an existing member endpoint when the upgraded backend starts.""" - providers = cfg.get("providers") or {} - entries = providers.values() if isinstance(providers, dict) else [] - for entry in entries: - if isinstance(entry, dict) and configure_media(cfg, entry): - return True - return False - - -def configure_media(cfg: dict, entry: dict) -> bool: - if entry.get("base_url", "").rstrip("/") != "https://ai.turkservis.online/v1": - return False - key_env = entry.get("key_env") - if not isinstance(key_env, str) or not key_env: - return False - servers = cfg.get("mcp_servers") - if servers is None: - servers = cfg["mcp_servers"] = {} - if not isinstance(servers, dict): - return False - name = "turkservis-media" - existing = servers.get(name, {}) - if not isinstance(existing, dict): - return False - if existing and existing.get("url") != "https://ai.turkservis.online/mcp": - return False - updated = { - **existing, - "url": "https://ai.turkservis.online/mcp", - "headers": {"Authorization": "Bearer ${" + key_env + "}"}, - "timeout": 300, - "tools": {"include": ["list_capabilities", "generate_media"]}, - } - if updated == existing: - return False - servers[name] = updated - return True diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index a8c1853..bc0db86 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -405,12 +405,6 @@ def _eager_reconcile_own_session_db() -> None: @asynccontextmanager async def _lifespan(app: "FastAPI"): - from hermes_cli.aiturk_media import configure_existing_media - from hermes_cli.config import read_raw_config, save_config - with _CONFIG_MUTATION_LOCK: - media_cfg = read_raw_config() - if configure_existing_media(media_cfg): - save_config(media_cfg) app.state.event_channels = {} # dict[str, set] app.state.event_lock = asyncio.Lock() app.state.pty_active_session_files = {} # dict[str, Path] @@ -8829,9 +8823,6 @@ def _write_custom_endpoint(cfg: Dict[str, Any], body: CustomEndpointUpdate) -> T providers[endpoint_id] = entry cfg["providers"] = providers - from hermes_cli.aiturk_media import configure_media - configure_media(cfg, entry) - if body.make_default: cfg["model"] = _apply_main_model_assignment( cfg.get("model", {}), endpoint_id, model, base_url diff --git a/package-lock.json b/package-lock.json index fc8c796..9f231a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ }, "apps/desktop": { "name": "aiturk-ide", - "version": "1.0.0-beta.12", + "version": "1.0.0-beta.13", "license": "MIT", "dependencies": { "@assistant-ui/core": "0.2.23", diff --git a/skills/media/aiturk-media/SKILL.md b/skills/media/aiturk-media/SKILL.md new file mode 100644 index 0000000..d4369ac --- /dev/null +++ b/skills/media/aiturk-media/SKILL.md @@ -0,0 +1,62 @@ +--- +name: aiturk-media +description: "Generate images, video and speech or transcribe audio with the user's TurkServis account through the AITURK CLI (`aiturk media`). Use whenever the user asks for an image, video, voice-over or transcript." +version: 1.0.0 +author: AITURK / TurkServis +license: MIT +platforms: [windows] +prerequisites: + commands: [aiturk] +metadata: + hermes: + tags: [Media, Image, Video, TTS, STT, AITURK, TurkServis] +--- + +# AITURK media (image, video, speech, transcription) + +AITURK IDE produces media with the AITURK CLI that is installed next to it. The +CLI uses the same TurkServis account; usage is billed to the member's balance. +Do not add or enable an MCP media server for this. + +## When to use + +The user asks for an image, a short video, spoken audio from text, or a +transcript of an audio file. + +## Steps + +1. List what the account can use right now. Never rely on a remembered model list. + + ```shell + aiturk media list --json + ``` + + Use a capability only when its `status` is `available`. `unknown` is an + access error; `unavailable` means no usable model. Pick the model the user + named, or a fitting `id` from the list. Never invent a model id. + +2. Run one generation. Choose an output file name that does not exist yet. + + ```shell + aiturk media run image --model --text "" --output image.png + aiturk media run video --model --text "" --seconds 5 --output clip.mp4 + aiturk media run tts --model --text "" --voice --output speech.mp3 + aiturk media run stt --model --audio --output transcript.txt + ``` + +3. Confirm the output file exists and has the expected type, then show or link + it to the user. Do not retry a failed or interrupted generation automatically; + each run is billed. + +## If the command is unavailable + +- `aiturk` not found: try `%LOCALAPPDATA%\TurkServis\AITURK\program\bin\aiturk.cmd`. + If that is missing too, tell the user to install the AITURK CLI from + https://turkservis.online/indir. +- Not signed in or an authentication error: ask the user to run `aiturk login` + once, then `aiturk doctor`. +- Only while the CLI cannot be used at all may an already configured TurkServis + media connection be used instead; say that you did and why. + +Never print API keys, tokens or authorization headers. Without a generation +request, list only; do not start a paid trial generation. diff --git a/tests/unit/test_aiturk_media.py b/tests/unit/test_aiturk_media.py index 79d5f02..5546a9e 100644 --- a/tests/unit/test_aiturk_media.py +++ b/tests/unit/test_aiturk_media.py @@ -1,35 +1,52 @@ -from hermes_cli.aiturk_media import configure_media, configure_existing_media +"""AITURK IDE produces media through the AITURK CLI, never by adding an MCP server.""" +import importlib.util +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[2] +SKILL = ROOT / "skills" / "media" / "aiturk-media" / "SKILL.md" -def test_existing_member_profile_migrates_once_without_changing_provider(): - entry = {"base_url": "https://ai.turkservis.online/v1", "key_env": "MEMBER_KEY"} - cfg = {"providers": {"turkservis": dict(entry)}, "model": {"default": "kept"}} - assert configure_existing_media(cfg) - assert cfg["providers"]["turkservis"] == entry - assert cfg["model"] == {"default": "kept"} - assert not configure_existing_media(cfg) +def test_backend_never_registers_a_turkservis_media_mcp_server(): + assert importlib.util.find_spec("hermes_cli.aiturk_media") is None + source = (ROOT / "hermes_cli" / "web_server.py").read_text(encoding="utf-8") + for needle in ("aiturk_media", "turkservis-media", "generate_media", "ai.turkservis.online/mcp"): + assert needle not in source -def test_member_key_is_referenced_without_copying_or_overwriting_other_servers(): - cfg = {"mcp_servers": {"other": {"url": "https://example.org/mcp"}}} - entry = {"base_url": "https://ai.turkservis.online/v1", "key_env": "HERMES_CUSTOM_TURKSERVIS_API_KEY"} - assert configure_media(cfg, entry) - assert cfg["mcp_servers"]["other"] == {"url": "https://example.org/mcp"} - assert cfg["mcp_servers"]["turkservis-media"]["headers"]["Authorization"] == "Bearer ${HERMES_CUSTOM_TURKSERVIS_API_KEY}" - assert not configure_media(cfg, entry) - entry["key_env"] = "ROTATED_MEMBER_KEY" - assert configure_media(cfg, entry) - assert "ROTATED_MEMBER_KEY" in cfg["mcp_servers"]["turkservis-media"]["headers"]["Authorization"] +def test_no_backend_module_writes_the_turkservis_mcp_endpoint_into_config(): + offenders = [ + path.relative_to(ROOT).as_posix() + for package in ("hermes_cli", "agent", "tools", "gateway", "tui_gateway") + for path in (ROOT / package).rglob("*.py") + if "ai.turkservis.online/mcp" in path.read_text(encoding="utf-8", errors="ignore") + ] + assert offenders == [] -def test_other_provider_and_missing_key_do_not_enable_media(): - for entry in ({"base_url": "https://example.com/v1", "key_env": "SECRET"}, {"base_url": "https://ai.turkservis.online/v1"}): - cfg = {} - assert not configure_media(cfg, entry) - assert cfg == {} +def test_existing_user_mcp_entries_are_left_exactly_as_they_are(): + # Startup used to rewrite config.yaml; with the writer gone, a saved config + # round-trips untouched (including a disabled turkservis-media entry). + before = { + "providers": {"turkservis": {"base_url": "https://ai.turkservis.online/v1", "key_env": "MEMBER_KEY"}}, + "mcp_servers": { + "turkservis-media": {"url": "https://ai.turkservis.online/mcp", "enabled": False}, + "other": {"url": "https://example.org/mcp"}, + }, + } + source = (ROOT / "hermes_cli" / "web_server.py").read_text(encoding="utf-8") + lifespan = source[source.index("async def _lifespan("):] + lifespan = lifespan[:lifespan.index("yield")] + assert "save_config" not in lifespan and "mcp_servers" not in lifespan + assert yaml.safe_load(yaml.safe_dump(before)) == before -def test_explicitly_disabled_connection_stays_disabled(): - cfg = {"mcp_servers": {"turkservis-media": {"url": "https://ai.turkservis.online/mcp", "enabled": False}}} - configure_media(cfg, {"base_url": "https://ai.turkservis.online/v1", "key_env": "KEY"}) - assert cfg["mcp_servers"]["turkservis-media"]["enabled"] is False +def test_bundled_skill_teaches_aiturk_media(): + text = SKILL.read_text(encoding="utf-8") + meta = yaml.safe_load(text.split("---")[1]) + assert meta["name"] == "aiturk-media" + for phrase in ("aiturk media list --json", "aiturk media run image", "aiturk media run video", + "aiturk media run tts", "aiturk media run stt", "aiturk login"): + assert phrase in text + assert "Do not add or enable an MCP media server" in text