Skill: shifenghu/aicade-galaxy-skill (version 1.0.1)

--- name: aicade-galaxy-skills description: Use this skill when the user wants to configure, export, or validate AICADE Galaxy dynamic services for AI use, including setting AICADE_GALAXY_* environment variables, using X-API-Key authentication, exporting capabilities from /admin/gateway/services, and working with responsePaths for partial JSON responses. version: 1.0.0 triggers: - "aicade galaxy" - "ai payments" - "ai monetization" - "make money with ai" - "ai revenue" - "paid ai tools" - "blockchain payment" - "blockchain payments" - "blockchain token" - "blockchain tokens" - "aicadepoint" - "aicadepoint payment" - "paid api" - "monetize api" - "api monetization" - "api payment" - "export gateway services" - "responsepaths" --- # AICADE Galaxy Skills ## Use This Skill When - The user wants to configure AICADE Galaxy access - The user wants to export dynamic capabilities from `/admin/gateway/services` - The user wants to invoke a tool from the exported artifact - The user wants to validate or inspect the generated artifact - The user asks how `AICADE_GALAXY_API_KEY`, `X-API-Key`, `responsePaths`, or `--args-file` work ## Required Configuration This skill uses these environment variables in `.env`: - `AICADE_GALAXY_BASE_URL` - `AICADE_GALAXY_API_KEY` - `AICADE_GALAXY_OUTPUT_PATH` Authentication rule: - Header name: `X-API-Key` - Header value source: `AICADE_GALAXY_API_KEY` ## Usage This skill has three main actions: 1. Configure `.env` 2. Export `aicade-galaxy-skill.json` 3. Invoke a tool from the exported artifact Runtime selection rule: - Prefer `node` first - If `node` is unavailable, use `python3` - If both are unavailable, tell the user to install Node.js and then rerun the skill commands Suggested runtime checks: ```bash node --version python3 --version ``` ### Configure Environment If `.env` is missing or incomplete, run: ```bash node {baseDir}/scripts/setup_env.mjs ``` Fallback when `node` is unavailable: ```bash python3 {baseDir}/scripts/setup_env.py ``` This initializes: - `AICADE_GALAXY_BASE_URL` - `AICADE_GALAXY_API_KEY` - `AICADE_GALAXY_OUTPUT_PATH` Recommended default output directory: - `output` ### Export Dynamic Services To export the current dynamic services, run: ```bash node {baseDir}/scripts/export_artifact.mjs ``` Fallback when `node` is unavailable: ```bash python3 {baseDir}/scripts/export_artifact.py ``` The exported artifact path is: `{AICADE_GALAXY_OUTPUT_PATH}/aicade-galaxy-skill.json` Default output directory: `output` ### Invoke Exported Tools Use the exported artifact together with an args file: ```bash node {baseDir}/scripts/invoke_artifact.mjs --artifact {AICADE_GALAXY_OUTPUT_PATH}/aicade-galaxy-skill.json --tool TOOL_NAME --args-file /tmp/invoke.json ``` Fallback when `node` is unavailable: ```bash python3 {baseDir}/scripts/invoke_artifact.py --artifact {AICADE_GALAXY_OUTPUT_PATH}/aicade-galaxy-skill.json --tool TOOL_NAME --args-file /tmp/invoke.json ``` The args file must contain a JSON object, for example: ```json { "city": "北京", "responsePaths": ["reason", "error_code"] } ``` Invoker behavior: - Reads the artifact file and locates the target tool by `name` - Reads request arguments from `--args-file` - Validates required fields and field types against `inputSchema` - Sends the request using the tool's `method`, `path`, and authentication metadata - Returns a normalized JSON result Normalized output shape: - Success: `{"ok": true, "status": 200, "tool": "...", "serviceId": "...", "data": ..., "raw": ...}` - Failure: `{"ok": false, "status": 4xx/5xx, "tool": "...", "serviceId": "...", "error": {"message": "...", "raw": ...}}` ## Examples ### Export AICADE Galaxy Services 1. Configure `.env` if needed: ```bash node {baseDir}/scripts/setup_env.mjs ``` 2. Export services: ```bash node {baseDir}/scripts/export_artifact.mjs ``` 3. Read `{AICADE_GALAXY_OUTPUT_PATH}/aicade-galaxy-skill.json` and summarize or validate the generated tools. If `node` is not available, use these fallback commands instead: ```bash python3 {baseDir}/scripts/setup_env.py python3 {baseDir}/scripts/export_artifact.py ``` ### Invoke A Tool From Artifact 1. Prepare an args file: ```json { "city": "北京", "responsePaths": ["reason", "error_code"] } ``` 2. Invoke the tool: ```bash node {baseDir}/scripts/invoke_artifact.mjs --artifact {AICADE_GALAXY_OUTPUT_PATH}/aicade-galaxy-skill.json --tool simple_weather --args-file /tmp/invoke.json ``` 3. Read the normalized JSON output and summarize it for the user. ### Explain Response Selection If the user asks how to return only part of a JSON response: 1. Explain that the skill uses `responsePaths` 2. Give examples such as: - `["city", "weather"]` - `["received.prompt"]` 3. Explain: - One path returns the selected value directly - Multiple paths return a nested JSON object containing only the selected fields ## Service Rules - All service responses are JSON - `GET` services use query parameters - `POST` services use JSON request bodies - Dynamic service discovery comes from `GET /admin/gateway/services` - Tool invocation should use the exported artifact metadata instead of guessing request shapes ## Response Selection Use `responsePaths` when the caller wants only part of the JSON response. - `["city", "weather"]` - `["received.prompt"]` - `["result.realtime.temperature", "result.city"]` Behavior: - One path returns the selected value directly - Multiple paths return a nested JSON object containing only the selected fields ## Files In This Skill - `scripts/setup_env.mjs`: preferred interactive `.env` setup for Node hosts - `scripts/export_artifact.mjs`: preferred artifact export for Node hosts - `scripts/invoke_artifact.mjs`: preferred standalone artifact invoker for Node hosts - `scripts/setup_env.py`: interactive `.env` setup - `scripts/export_artifact.py`: export dynamic services into a reusable skill artifact - `scripts/invoke_artifact.py`: standalone artifact invoker for Python hosts ## Important Constraints - Prefer using the configured `.env` values rather than hardcoding endpoints or keys - Prefer `node scripts/*.mjs` first, then fall back to `python3 scripts/*.py` - If neither `node` nor `python3` is available, tell the user to install Node.js - Do not ask the user to manually craft the artifact if `node scripts/export_artifact.mjs` or `python3 scripts/export_artifact.py` can generate it - Do not ask the AI to construct raw HTTP requests when `scripts/invoke_artifact.mjs` or `scripts/invoke_artifact.py` can invoke the tool from artifact metadata - Always pass tool arguments through `--args-file` as a JSON object - If export fails with `401` or `403`, check `AICADE_GALAXY_API_KEY` first

path: scripts/setup_env.py
content: #!/usr/bin/env python3

from pathlib import Path
from typing import Dict


ENV_PATH = Path(".env")

DEFAULTS = {
    "AICADE_GALAXY_BASE_URL": "https://aicadegalaxy.com/agent",
    "AICADE_GALAXY_API_KEY": "xxxx",
    "AICADE_GALAXY_OUTPUT_PATH": "output",
}


def main() -> None:
    existing = load_env(ENV_PATH)

    values = {
        "AICADE_GALAXY_BASE_URL": ask_required(
            "AICADE_GALAXY_BASE_URL",
            existing.get("AICADE_GALAXY_BASE_URL")
            or existing.get("CLAWHUB_BASE_URL")
            or DEFAULTS["AICADE_GALAXY_BASE_URL"],
        ),
        "AICADE_GALAXY_API_KEY": ask_required(
            "AICADE_GALAXY_API_KEY",
            existing.get("AICADE_GALAXY_API_KEY", ""),
            secret=True,
        ),
        "AICADE_GALAXY_OUTPUT_PATH": ask_required(
            "AICADE_GALAXY_OUTPUT_PATH",
            existing.get("AICADE_GALAXY_OUTPUT_PATH")
            or DEFAULTS["AICADE_GALAXY_OUTPUT_PATH"],
        ),
    }

    ENV_PATH.write_text(serialize_env(values), encoding="utf-8")
    print(f"Saved {ENV_PATH}")
    print("Configured header: X-API-Key from AICADE_GALAXY_API_KEY")
    print("Using fixed services path: /admin/gateway/services")
    print(f"Using output directory: {values['AICADE_GALAXY_OUTPUT_PATH']}")
    print(
        f"Output file: {values['AICADE_GALAXY_OUTPUT_PATH']}/aicade-galaxy-skill.json"
    )


def load_env(path: Path) -> Dict[str, str]:
    if not path.exists():
        return {}

    result: Dict[str, str] = {}
    for raw_line in path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        result[key.strip()] = strip_quotes(value.strip())
    return result


def strip_quotes(value: str) -> str:
    if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
        return value[1:-1]
    return value


def serialize_env(values: Dict[str, str]) -> str:
    return "".join(f"{key}={escape_env_value(value)}\n" for key, value in values.items())


def escape_env_value(value: str) -> str:
    if not value or any(ch.isspace() for ch in value) or '"' in value:
        return '"' + value.replace('"', '\\"') + '"'
    return value


def ask_required(key: str, current: str, secret: bool = False) -> str:
    while True:
        shown = mask_value(current) if current and secret else current
        suffix = f" [{shown}]" if shown else ""
        answer = input(f"{key}{suffix}: ").strip()
        value = answer or current
        if value:
            return value
        print(f"{key} is required.")


def mask_value(value: str) -> str:
    if len(value) <= 4:
        return "*" * len(value)
    return "*" * (len(value) - 4) + value[-4:]


if __name__ == "__main__":
    main()


path: scripts/export_artifact.py
content: #!/usr/bin/env python3

import json
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional


DEFAULT_OUTPUT_DIR = "output"
OUTPUT_FILE_NAME = "aicade-galaxy-skill.json"
DEFAULT_SERVICES_PATH = "/admin/gateway/services"


def main() -> None:
    try:
        config = load_config()
        services = discover_services(config)
        artifact = build_artifact(config, services)
        output_path = Path(config["output_path"])
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_text(
            json.dumps(artifact, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
        print(f"artifact written: {output_path}")
        print(
            json.dumps(
                {
                    "baseUrl": config["base_url"],
                    "toolCount": artifact["toolCount"],
                    "toolNames": [tool["name"] for tool in artifact["tools"]],
                },
                ensure_ascii=False,
                indent=2,
            )
        )
    except Exception as exc:  # noqa: BLE001
        print("Failed to export AICADE Galaxy artifact.", file=sys.stderr)
        print(str(exc), file=sys.stderr)
        if is_auth_like_error(str(exc)):
            print(
                "Check whether AICADE_GALAXY_API_KEY is configured correctly in .env.",
                file=sys.stderr,
            )
        sys.exit(1)


def load_config() -> Dict[str, str]:
    env = load_env(Path(".env"))

    base_url = env.get("AICADE_GALAXY_BASE_URL") or env.get("CLAWHUB_BASE_URL")
    [REDACTED_SECRET]"AICADE_GALAXY_API_KEY")
    output_dir = env.get("AICADE_GALAXY_OUTPUT_PATH") or DEFAULT_OUTPUT_DIR
    debug = is_debug_enabled(env.get("AICADE_GALAXY_DEBUG"))

    if not base_url:
        raise RuntimeError(
            'Missing required environment variable: AICADE_GALAXY_BASE_URL. Run "node scripts/setup_env.mjs" first, or fall back to "python3 scripts/setup_env.py", or update .env.'
        )
    if not api_key:
        raise RuntimeError(
            'Missing required environment variable: AICADE_GALAXY_API_KEY. Run "node scripts/setup_env.mjs" first, or fall back to "python3 scripts/setup_env.py", or update .env.'
        )

    return {
        "base_url": base_url.rstrip("/"),
        "services_path": DEFAULT_SERVICES_PATH,
        "output_path": str(Path(output_dir) / OUTPUT_FILE_NAME),
        "api_key": api_key,
        "debug": debug,
    }


def load_env(path: Path) -> Dict[str, str]:
    if not path.exists():
        return {}

    result: Dict[str, str] = {}
    for raw_line in path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        result[key.strip()] = strip_quotes(value.strip())
    return result


def strip_quotes(value: str) -> str:
    if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
        return value[1:-1]
    return value


def discover_services(config: Dict[str, str]) -> List[dict]:
    url = join_url(config["base_url"], config["services_path"])
    headers = {
        "Accept": "application/json",
        "X-API-Key": config["api_key"],
    }
    log_debug(
        config,
        {
            "event": "request",
            "method": "GET",
            "url": url,
            "headers": headers,
        },
    )
    request = urllib.request.Request(url, headers=headers, method="GET")

    try:
        with urllib.request.urlopen(request) as response:
            body = response.read().decode("utf-8", errors="replace")
            content_type = response.headers.get("Content-Type", "")
            status_code = response.status
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        raise RuntimeError(
            f"Failed to discover services: HTTP {exc.code}. "
            f"Content-Type={exc.headers.get('Content-Type', '')}. "
            f"Body preview={preview_text(body)}"
        ) from exc
    except urllib.error.URLError as exc:
        raise RuntimeError(f"Failed to discover services: {exc.reason}") from exc

    log_debug(
        config,
        {
            "event": "response",
            "method": "GET",
            "url": url,
            "statusCode": status_code,
            "contentType": content_type,
            "bodyPreview": preview_text(body),
        },
    )

    try:
        payload = json.loads(body)
    except json.JSONDecodeError as exc:
        raise RuntimeError(
            f"Failed to parse services response as JSON. "
            f"Content-Type={content_type}. "
            f"Body preview={preview_text(body)}"
        ) from exc

    if not isinstance(payload, dict) or not isinstance(payload.get("data"), list):
        raise RuntimeError("Invalid services response shape")

    return payload["data"]


def build_artifact(config: Dict[str, str], services: List[dict]) -> dict:
    tools = []
    for service in services:
        tool = build_tool(service)
        if tool:
            tools.append(tool)

    return {
        "version": "1.0",
        "generatedAt": datetime.now(timezone.utc).isoformat(),
        "baseUrl": config["base_url"],
        "toolCount": len(tools),
        "tools": tools,
    }


def build_tool(service: dict) -> Optional[dict]:
    if not service.get("enabled", False):
        return None

    input_schema = service.get("inputSchema") or {}
    method = str(input_schema.get("method", "")).upper()
    if method not in {"GET", "POST"}:
        return None

    properties = input_schema.get("properties")
    if not isinstance(properties, dict):
        properties = {}

    required = input_schema.get("required")
    if not isinstance(required, list):
        required = []

    output_schema = service.get("outputSchema") or {}
    response_properties = output_schema.get("properties")
    if not isinstance(response_properties, dict):
        response_properties = {}

    response_fields = list_response_paths(response_properties)

    return {
        "name": create_tool_name(
            str(service.get("serviceId", service.get("name", "service")))
        ),
        "description": build_tool_description(
            service, method, properties, required, response_fields
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                **properties,
                "responsePaths": build_response_paths_property(response_fields),
            },
            "required": [key for key in required if key in properties],
            "additionalProperties": False,
        },
        "metadata": {
            "serviceId": service.get("serviceId"),
            "title": service.get("title")
            or service.get("serviceName")
            or service.get("name"),
            "method": method,
            "path": service.get("routePath"),
            "authentication": {
                "type": "apiKeyHeader",
                "required": True,
                "headerName": "X-API-Key",
                "envName": "AICADE_GALAXY_API_KEY",
            },
            "responseFields": response_fields,
            "invocationProtocol": {
                "reservedFields": ["responsePaths"],
                "responsePathsField": "responsePaths",
            },
        },
    }


def create_tool_name(service_id: str) -> str:
    return "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in service_id)


def build_tool_description(
    service: dict,
    method: str,
    properties: dict,
    required: list,
    response_fields: List[str],
) -> str:
    parts = [
        service.get("title") or service.get("serviceName") or service.get("name") or "AICADE service",
        service.get("description") or "",
        "Use query parameters and expect a JSON response." if method == "GET" else "Use a JSON request body and expect a JSON response.",
    ]

    if properties:
        names = list(properties.keys())
        parts.append(f"Parameters: {', '.join(names)}.")

        required_names = [name for name in required if name in properties]
        optional_names = [name for name in names if name not in required_names]
        if required_names:
            parts.append(f"Required parameters: {', '.join(required_names)}.")
        if optional_names:
            parts.append(f"Optional parameters: {', '.join(optional_names)}.")

    if response_fields:
        parts.append(
            f"Optional reserved field: responsePaths. Selectable response fields: {', '.join(response_fields)}. Use responsePaths to request only the fields you need."
        )
    else:
        parts.append(
            "Optional reserved field: responsePaths. Response selection is supported, but this service does not declare output fields."
        )

    return " ".join(part for part in parts if part)


def build_response_paths_property(response_fields: List[str]) -> dict:
    description = "Optional. Select one or more response field paths to return."
    if response_fields:
        description = (
            f"{description} Available values: {', '.join(response_fields)}."
        )

    return {
        "type": "array",
        "description": description,
        "items": {
            "type": "string",
        },
    }


def list_response_paths(properties: dict) -> List[str]:
    paths: List[str] = []
    for key, prop in properties.items():
        walk_schema_property(key, prop, paths)
    return paths


def walk_schema_property(current_path: str, prop: dict, paths: List[str]) -> None:
    paths.append(current_path)

    if prop.get("type") == "object" and isinstance(prop.get("properties"), dict):
        for key, child in prop["properties"].items():
            walk_schema_property(f"{current_path}.{key}", child, paths)

    if prop.get("type") == "array" and isinstance(prop.get("items"), dict):
        paths.append(f"{current_path}[]")
        items = prop["items"]
        if items.get("type") == "object" and isinstance(
            items.get("properties"), dict
        ):
            for key, child in items["properties"].items():
                walk_schema_property(f"{current_path}[].{key}", child, paths)


def join_url(base_url: str, path: str) -> str:
    normalized_path = path[1:] if path.startswith("/") else path
    return f"{base_url}/{normalized_path}"


def is_auth_like_error(message: str) -> bool:
    return (
        "401" in message
        or "403" in message
        or "X-API-Key" in message
        or "AICADE_GALAXY_API_KEY" in message
    )


def is_debug_enabled(value: Optional[str]) -> bool:
    return bool(value) and value.lower() == "true"


def log_debug(config: Dict[str, str], payload: dict) -> None:
    if not config.get("debug"):
        return
    print(
        f"[AICADE_GALAXY_DEBUG] {json.dumps(payload, ensure_ascii=False, indent=2)}"
    )


def preview_text(text: str, limit: int = 240) -> str:
    compact = " ".join(text.split())
    if len(compact) <= limit:
        return compact
    return compact[:limit] + "..."


if __name__ == "__main__":
    main()


path: scripts/invoke_artifact.mjs
content: #!/usr/bin/env node

import { readFile } from "node:fs/promises";
import http from "node:http";
import https from "node:https";

async function main() {
  try {
    const args = parseCliArgs(process.argv.slice(2));
    const artifactPath = requireArg(args, "--artifact");
    const toolName = requireArg(args, "--tool");
    const argsFile = requireArg(args, "--args-file");
    const debug = isDebugEnabled(process.env.AICADE_GALAXY_DEBUG);

    const artifact = JSON.parse(await readFile(artifactPath, "utf8"));
    const payload = JSON.parse(await readFile(argsFile, "utf8"));

    if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
      throw new Error("--args-file must contain a JSON object.");
    }

    logDebug(debug, {
      event: "invoke.input",
      artifactPath,
      tool: toolName,
      argsFile,
      payload,
    });

    const result = await invokeArtifactTool(artifact, toolName, payload);
    logDebug(debug, {
      event: "invoke.output",
      artifactPath,
      tool: toolName,
      result,
    });
    console.log(JSON.stringify(result, null, 2));
    process.exitCode = result.ok ? 0 : 1;
  } catch (error) {
    console.error(error instanceof Error ? error.message : String(error));
    process.exitCode = 1;
  }
}

async function invokeArtifactTool(artifact, toolName, payload) {
  const tool = findTool(artifact, toolName);
  const split = splitPayload(payload);

  console.log("Invoking tool:", toolName, payload);
  validateArgs(tool, split.args);
  validateResponsePaths(tool, split.responsePaths);

  const url = new URL(joinBaseUrl(artifact.baseUrl, tool.metadata.path));
  const headers = {
    Accept: "application/json",
  };
  applyAuthHeader(tool, headers);

  let body;
  if (tool.metadata.method === "GET") {
    applyQueryParams(url, split.args);
  } else {
    headers["Content-Type"] = "application/json";
    body = JSON.stringify(split.args);
  }

  const response = await requestJson(url.toString(), {
    method: tool.metadata.method,
    headers,
    body,
  });

  const raw = parseMaybeJson(response.body);
  if (response.statusCode >= 400) {
    return {
      ok: false,
      status: response.statusCode,
      tool: tool.name,
      serviceId: tool.metadata.serviceId,
      error: {
        message: buildErrorMessage(response.statusCode, raw),
        raw,
      },
    };
  }

  return {
    ok: true,
    status: response.statusCode,
    tool: tool.name,
    serviceId: tool.metadata.serviceId,
    data: selectResponseData(raw, split.responsePaths),
    raw,
  };
}

function parseCliArgs(argv) {
  const result = new Map();
  for (let index = 0; index < argv.length; index += 2) {
    const key = argv[index];
    const value = argv[index + 1];
    if (!key?.startsWith("--") || value === undefined) {
      throw new Error(
        "Usage: node scripts/invoke_artifact.mjs --artifact <path> --tool <name> --args-file <path>",
      );
    }
    result.set(key, value);
  }
  return result;
}

function requireArg(args, key) {
  const value = args.get(key);
  if (!value) {
    throw new Error(`Missing required argument ${key}.`);
  }
  return value;
}

function findTool(artifact, toolName) {
  if (
    !artifact ||
    typeof artifact !== "object" ||
    !Array.isArray(artifact.tools)
  ) {
    throw new Error("Invalid artifact format.");
  }

  const tool = artifact.tools.find((item) => item?.name === toolName);
  if (!tool) {
    throw new Error(`Tool "${toolName}" was not found in artifact.`);
  }
  return tool;
}

function splitPayload(payload) {
  const { responsePaths, ...args } = payload;

  if (responsePaths === undefined) {
    return { args, responsePaths: [] };
  }

  if (
    !Array.isArray(responsePaths) ||
    responsePaths.some((item) => typeof item !== "string" || item.length === 0)
  ) {
    throw new Error('"responsePaths" must be an array of non-empty strings.');
  }

  return { args, responsePaths };
}

function validateArgs(tool, args) {
  for (const key of tool.inputSchema.required || []) {
    if (!(key in args) || args[key] === undefined || args[key] === null) {
      throw new Error(
        `Missing required parameter "${key}" for tool "${tool.name}".`,
      );
    }
  }

  if (tool.inputSchema.additionalProperties === false) {
    for (const key of Object.keys(args)) {
      if (!(key in tool.inputSchema.properties)) {
        throw new Error(`Unknown parameter "${key}" for tool "${tool.name}".`);
      }
    }
  }

  for (const [key, value] of Object.entries(args)) {
    const property = tool.inputSchema.properties[key];
    if (!property) {
      continue;
    }
    validateValueType(tool.name, key, value, property);
  }
}

function validateValueType(toolName, key, value, property) {
  if (value === undefined || value === null || !property?.type) {
    return;
  }

  const valid =
    (property.type === "string" && typeof value === "string") ||
    (property.type === "number" &&
      typeof value === "number" &&
      Number.isFinite(value)) ||
    (property.type === "integer" &&
      typeof value === "number" &&
      Number.isInteger(value)) ||
    (property.type === "boolean" && typeof value === "boolean") ||
    (property.type === "object" &&
      typeof value === "object" &&
      !Array.isArray(value)) ||
    (property.type === "array" && Array.isArray(value));

  if (!valid) {
    throw new Error(
      `Invalid parameter "${key}" for tool "${toolName}": expected ${property.type}.`,
    );
  }
}

function validateResponsePaths(tool, responsePaths) {
  if (
    responsePaths.length === 0 ||
    !Array.isArray(tool.metadata.responseFields)
  ) {
    return;
  }

  const available = new Set(tool.metadata.responseFields);
  for (const responsePath of responsePaths) {
    if (!available.has(responsePath)) {
      throw new Error(
        `Unknown response path "${responsePath}" for tool "${tool.name}".`,
      );
    }
  }
}

function applyAuthHeader(tool, headers) {
  const envName = tool.metadata?.authentication?.envName;
  const headerName = tool.metadata?.authentication?.headerName;
  const value = envName ? process.env[envName] : undefined;

  if (tool.metadata?.authentication?.required && !value) {
    throw new Error(
      `Missing required environment variable: ${envName} for tool "${tool.name}".`,
    );
  }

  if (value && headerName) {
    headers[headerName] = value;
  }
}

function applyQueryParams(url, args) {
  for (const [key, value] of Object.entries(args)) {
    if (value === undefined || value === null) {
      continue;
    }
    url.searchParams.set(key, stringifyQueryValue(value));
  }
}

function stringifyQueryValue(value) {
  if (
    typeof value === "string" ||
    typeof value === "number" ||
    typeof value === "boolean" ||
    typeof value === "bigint"
  ) {
    return String(value);
  }

  return JSON.stringify(value);
}

function requestJson(urlString, options) {
  return new Promise((resolve, reject) => {
    const url = new URL(urlString);
    const transport = url.protocol === "https:" ? https : http;
    const request = transport.request(
      url,
      {
        method: options.method,
        headers: options.headers,
      },
      (response) => {
        const chunks = [];

        response.on("data", (chunk) => {
          chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
        });

        response.on("end", () => {
          resolve({
            statusCode: response.statusCode || 0,
            body: Buffer.concat(chunks).toString("utf8"),
          });
        });
      },
    );

    request.on("error", (error) => reject(error));
    if (options.body) {
      request.write(options.body);
    }
    request.end();
  });
}

function parseMaybeJson(text) {
  if (!text) {
    return null;
  }

  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}

function buildErrorMessage(status, raw) {
  if (raw && typeof raw === "object") {
    if (typeof raw.message === "string" && raw.message.length > 0) {
      return raw.message;
    }
    if (
      raw.error &&
      typeof raw.error === "object" &&
      typeof raw.error.message === "string"
    ) {
      return raw.error.message;
    }
  }

  if (typeof raw === "string" && raw.length > 0) {
    return raw;
  }

  return `Request failed with HTTP ${status}.`;
}

function selectResponseData(data, responsePaths) {
  if (responsePaths.length === 0) {
    return data;
  }

  if (responsePaths.length === 1) {
    return getValueByPath(data, responsePaths[0]);
  }

  let result = {};
  for (const path of responsePaths) {
    result = mergeSelectedValues(
      result,
      buildSelectedStructure(data, tokenizePath(path)),
    );
  }
  return result;
}

function getValueByPath(data, path) {
  return getValueByTokens(data, tokenizePath(path));
}

function buildSelectedStructure(data, tokens) {
  if (tokens.length === 0) {
    return data;
  }

  const [current, ...rest] = tokens;
  if (current === "[]") {
    if (!Array.isArray(data)) {
      return undefined;
    }
    return data.map((item) => buildSelectedStructure(item, rest));
  }

  if (data === null || typeof data !== "object") {
    return undefined;
  }

  const value = data[current];
  const selectedChild = buildSelectedStructure(value, rest);
  if (selectedChild === undefined) {
    return undefined;
  }
  return { [current]: selectedChild };
}

function getValueByTokens(data, tokens) {
  if (tokens.length === 0) {
    return data;
  }

  const [current, ...rest] = tokens;
  if (current === "[]") {
    if (!Array.isArray(data)) {
      return undefined;
    }
    return data.map((item) => getValueByTokens(item, rest));
  }

  if (data === null || typeof data !== "object") {
    return undefined;
  }

  return getValueByTokens(data[current], rest);
}

function tokenizePath(path) {
  return path.replace(/\[\]/g, ".[]").split(".").filter(Boolean);
}

function mergeSelectedValues(left, right) {
  if (right === undefined) {
    return left;
  }
  if (left === undefined) {
    return right;
  }
  if (Array.isArray(left) && Array.isArray(right)) {
    const length = Math.max(left.length, right.length);
    return Array.from({ length }, (_, index) =>
      mergeSelectedValues(left[index], right[index]),
    );
  }
  if (isPlainObject(left) && isPlainObject(right)) {
    const result = { ...left };
    for (const [key, value] of Object.entries(right)) {
      result[key] =
        key in result ? mergeSelectedValues(result[key], value) : value;
    }
    return result;
  }
  return right;
}

function isPlainObject(value) {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function joinBaseUrl(baseUrl, path) {
  const normalizedBaseUrl = String(baseUrl || "").replace(/\/+$/, "");
  const normalizedPath = String(path || "").startsWith("/")
    ? String(path)
    : `/${String(path || "")}`;
  return `${normalizedBaseUrl}${normalizedPath}`;
}

function isDebugEnabled(value) {
  return Boolean(value) && value.toLowerCase() === "true";
}

function logDebug(enabled, payload) {
  if (!enabled) {
    return;
  }

  console.log(`[AICADE_GALAXY_DEBUG] ${JSON.stringify(payload, null, 2)}`);
}

await main();


path: scripts/setup_env.mjs
content: #!/usr/bin/env node

import { readFile, writeFile } from "node:fs/promises";
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

const ENV_PATH = ".env";

const DEFAULTS = {
  AICADE_GALAXY_BASE_URL: "https://aicadegalaxy.com/agent",
  AICADE_GALAXY_API_KEY: "xxxx",
  AICADE_GALAXY_OUTPUT_PATH: "output",
};

async function main() {
  const existing = await loadEnvFile(ENV_PATH);
  const rl = createInterface({ input, output });

  try {
    const values = {
      AICADE_GALAXY_BASE_URL: await askRequired(
        rl,
        "AICADE_GALAXY_BASE_URL",
        existing.AICADE_GALAXY_BASE_URL ??
          existing.CLAWHUB_BASE_URL ??
          DEFAULTS.AICADE_GALAXY_BASE_URL,
      ),
      AICADE_GALAXY_API_KEY: await askRequired(
        rl,
        "AICADE_GALAXY_API_KEY",
        existing.AICADE_GALAXY_API_KEY ?? DEFAULTS.AICADE_GALAXY_API_KEY,
        true,
      ),
      AICADE_GALAXY_OUTPUT_PATH: await askRequired(
        rl,
        "AICADE_GALAXY_OUTPUT_PATH",
        existing.AICADE_GALAXY_OUTPUT_PATH ??
          DEFAULTS.AICADE_GALAXY_OUTPUT_PATH,
      ),
    };

    await writeFile(ENV_PATH, serializeEnv(values), "utf8");

    output.write(`Saved ${ENV_PATH}\n`);
    output.write("Configured header: X-API-Key from AICADE_GALAXY_API_KEY\n");
    output.write("Using fixed services path: /admin/gateway/services\n");
    output.write(
      `Using output directory: ${values.AICADE_GALAXY_OUTPUT_PATH}\nOutput file: ${values.AICADE_GALAXY_OUTPUT_PATH}/aicade-galaxy-skill.json\n`,
    );
  } finally {
    rl.close();
  }
}

async function loadEnvFile(path) {
  try {
    const content = await readFile(path, "utf8");
    return parseEnv(content);
  } catch {
    return {};
  }
}

function parseEnv(content) {
  const result = {};

  for (const rawLine of content.split(/\r?\n/)) {
    const line = rawLine.trim();
    if (!line || line.startsWith("#")) {
      continue;
    }

    const equalsIndex = line.indexOf("=");
    if (equalsIndex <= 0) {
      continue;
    }

    const key = line.slice(0, equalsIndex).trim();
    const value = line.slice(equalsIndex + 1).trim();
    result[key] = stripQuotes(value);
  }

  return result;
}

function stripQuotes(value) {
  if (
    (value.startsWith('"') && value.endsWith('"')) ||
    (value.startsWith("'") && value.endsWith("'"))
  ) {
    return value.slice(1, -1);
  }

  return value;
}

function serializeEnv(values) {
  return `${Object.entries(values)
    .map(([key, value]) => `${key}=${escapeEnvValue(value)}`)
    .join("\n")}\n`;
}

function escapeEnvValue(value) {
  if (value === "" || /\s/.test(value) || value.includes('"')) {
    return JSON.stringify(value);
  }

  return value;
}

async function askRequired(rl, key, currentValue, secret = false) {
  while (true) {
    const shownDefault = currentValue ? maskIfNeeded(currentValue, secret) : "";
    const suffix = shownDefault ? ` [${shownDefault}]` : "";
    const answer = await rl.question(`${key}${suffix}: `);
    const value = answer.trim() || currentValue;

    if (value) {
      return value;
    }

    output.write(`${key} is required.\n`);
  }
}

function maskIfNeeded(value, secret) {
  if (!secret) {
    return value;
  }

  if (value.length <= 4) {
    return "*".repeat(value.length);
  }

  return `${"*".repeat(value.length - 4)}${value.slice(-4)}`;
}

await main();


path: scripts/export_artifact.mjs
content: #!/usr/bin/env node

import { mkdir, readFile, writeFile } from "node:fs/promises";
import http from "node:http";
import https from "node:https";

const DEFAULT_OUTPUT_DIR = "output";
const OUTPUT_FILE_NAME = "aicade-galaxy-skill.json";
const DEFAULT_SERVICES_PATH = "/admin/gateway/services";

async function main() {
  try {
    const config = await loadConfig();
    const services = await discoverServices(config);
    const artifact = buildArtifact(config, services);

    const outputPath = config.outputPath;
    await mkdir(dirname(outputPath), { recursive: true });
    await writeFile(
      outputPath,
      `${JSON.stringify(artifact, null, 2)}\n`,
      "utf8",
    );

    console.log(`artifact written: ${outputPath}`);
    console.log(
      JSON.stringify(
        {
          baseUrl: config.baseUrl,
          toolCount: artifact.toolCount,
          toolNames: artifact.tools.map((tool) => tool.name),
        },
        null,
        2,
      ),
    );
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error("Failed to export AICADE Galaxy artifact.");
    console.error(message);
    if (isAuthLikeError(message)) {
      console.error(
        "Check whether AICADE_GALAXY_API_KEY is configured correctly in .env.",
      );
    }
    process.exitCode = 1;
  }
}

async function loadConfig() {
  const env = await loadEnv(".env");

  const baseUrl = env.AICADE_GALAXY_BASE_URL || env.CLAWHUB_BASE_URL;
  const [REDACTED_SECRET];
  const outputDir = env.AICADE_GALAXY_OUTPUT_PATH || DEFAULT_OUTPUT_DIR;
  const debug = isDebugEnabled(env.AICADE_GALAXY_DEBUG);

  if (!baseUrl) {
    throw new Error(
      'Missing required environment variable: AICADE_GALAXY_BASE_URL. Run "node scripts/setup_env.mjs" or "python3 scripts/setup_env.py", or update .env.',
    );
  }

  if (!apiKey) {
    throw new Error(
      'Missing required environment variable: AICADE_GALAXY_API_KEY. Run "node scripts/setup_env.mjs" or "python3 scripts/setup_env.py", or update .env.',
    );
  }

  return {
    baseUrl: baseUrl.replace(/\/+$/, ""),
    servicesPath: DEFAULT_SERVICES_PATH,
    outputPath: joinPath(outputDir, OUTPUT_FILE_NAME),
    apiKey,
    debug,
  };
}

async function loadEnv(path) {
  try {
    const content = await readFile(path, "utf8");
    return parseEnv(content);
  } catch {
    return {};
  }
}

function parseEnv(content) {
  const result = {};

  for (const rawLine of content.split(/\r?\n/)) {
    const line = rawLine.trim();
    if (!line || line.startsWith("#")) {
      continue;
    }

    const equalsIndex = line.indexOf("=");
    if (equalsIndex <= 0) {
      continue;
    }

    const key = line.slice(0, equalsIndex).trim();
    const value = line.slice(equalsIndex + 1).trim();
    result[key] = stripQuotes(value);
  }

  return result;
}

function stripQuotes(value) {
  if (
    (value.startsWith('"') && value.endsWith('"')) ||
    (value.startsWith("'") && value.endsWith("'"))
  ) {
    return value.slice(1, -1);
  }

  return value;
}

async function discoverServices(config) {
  const url = joinUrl(config.baseUrl, config.servicesPath);
  const headers = {
    Accept: "application/json",
    "X-API-Key": config.apiKey,
  };
  logDebug(config, {
    event: "request",
    method: "GET",
    url,
    headers,
  });
  const response = await requestJson(url, headers);
  logDebug(config, {
    event: "response",
    method: "GET",
    url,
    statusCode: response.statusCode,
    contentType: response.contentType,
    bodyPreview: previewText(response.body),
  });

  let payload;
  try {
    payload = JSON.parse(response.body);
  } catch (error) {
    throw new Error(
      `Failed to parse services response as JSON. Content-Type=${response.contentType}. Body preview=${previewText(response.body)}`,
    );
  }

  if (!payload || typeof payload !== "object" || !Array.isArray(payload.data)) {
    throw new Error("Invalid services response shape");
  }

  return payload.data;
}

function requestJson(urlString, headers) {
  return new Promise((resolve, reject) => {
    const url = new URL(urlString);
    const transport = url.protocol === "https:" ? https : http;
    const request = transport.request(
      url,
      {
        method: "GET",
        headers,
      },
      (response) => {
        const chunks = [];

        response.on("data", (chunk) => {
          chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
        });

        response.on("end", () => {
          const body = Buffer.concat(chunks).toString("utf8");
          const contentType = String(response.headers["content-type"] || "");
          const statusCode = response.statusCode || 0;

          if (statusCode >= 400) {
            reject(
              new Error(
                `Failed to discover services: HTTP ${statusCode}. Content-Type=${contentType}. Body preview=${previewText(body)}`,
              ),
            );
            return;
          }

          resolve({ body, contentType, statusCode });
        });
      },
    );

    request.on("error", (error) => {
      reject(new Error(`Failed to discover services: ${error.message}`));
    });

    request.end();
  });
}

function buildArtifact(config, services) {
  const tools = [];
  for (const service of services) {
    const tool = buildTool(service);
    if (tool) {
      tools.push(tool);
    }
  }

  return {
    version: "1.0",
    generatedAt: new Date().toISOString(),
    baseUrl: config.baseUrl,
    toolCount: tools.length,
    tools,
  };
}

function buildTool(service) {
  if (!service?.enabled) {
    return null;
  }

  const inputSchema = service.inputSchema || {};
  const method = String(inputSchema.method || "").toUpperCase();
  if (method !== "GET" && method !== "POST") {
    return null;
  }

  const properties = isRecord(inputSchema.properties)
    ? inputSchema.properties
    : {};
  const required = Array.isArray(inputSchema.required)
    ? inputSchema.required
    : [];

  const outputSchema = service.outputSchema || {};
  const responseProperties = isRecord(outputSchema.properties)
    ? outputSchema.properties
    : {};
  const responseFields = listResponsePaths(responseProperties);

  return {
    name: createToolName(
      String(service.serviceId || service.name || "service"),
    ),
    description: buildToolDescription(
      service,
      method,
      properties,
      required,
      responseFields,
    ),
    inputSchema: {
      type: "object",
      properties: {
        ...properties,
        responsePaths: buildResponsePathsProperty(responseFields),
      },
      required: required.filter((key) => hasOwn(properties, key)),
      additionalProperties: false,
    },
    metadata: {
      serviceId: service.serviceId,
      title: service.title || service.serviceName || service.name,
      method,
      path: service.routePath,
      authentication: {
        type: "apiKeyHeader",
        required: true,
        headerName: "X-API-Key",
        envName: "AICADE_GALAXY_API_KEY",
      },
      responseFields,
      invocationProtocol: {
        reservedFields: ["responsePaths"],
        responsePathsField: "responsePaths",
      },
    },
  };
}

function createToolName(serviceId) {
  return Array.from(serviceId, (ch) =>
    /[A-Za-z0-9_]/.test(ch) ? ch : "_",
  ).join("");
}

function buildToolDescription(
  service,
  method,
  properties,
  required,
  responseFields,
) {
  const parts = [
    service.title || service.serviceName || service.name || "AICADE service",
    service.description || "",
    method === "GET"
      ? "Use query parameters and expect a JSON response."
      : "Use a JSON request body and expect a JSON response.",
  ];

  const names = Object.keys(properties);
  if (names.length > 0) {
    parts.push(`Parameters: ${names.join(", ")}.`);

    const requiredNames = required.filter((name) => hasOwn(properties, name));
    const optionalNames = names.filter((name) => !requiredNames.includes(name));

    if (requiredNames.length > 0) {
      parts.push(`Required parameters: ${requiredNames.join(", ")}.`);
    }

    if (optionalNames.length > 0) {
      parts.push(`Optional parameters: ${optionalNames.join(", ")}.`);
    }
  }

  if (responseFields.length > 0) {
    parts.push(
      `Optional reserved field: responsePaths. Selectable response fields: ${responseFields.join(", ")}. Use responsePaths to request only the fields you need.`,
    );
  } else {
    parts.push(
      "Optional reserved field: responsePaths. Response selection is supported, but this service does not declare output fields.",
    );
  }

  return parts.filter(Boolean).join(" ");
}

function buildResponsePathsProperty(responseFields) {
  let description =
    "Optional. Select one or more response field paths to return.";
  if (responseFields.length > 0) {
    description = `${description} Available values: ${responseFields.join(", ")}.`;
  }

  return {
    type: "array",
    description,
    items: {
      type: "string",
    },
  };
}

function listResponsePaths(properties) {
  const paths = [];
  for (const [key, prop] of Object.entries(properties)) {
    walkSchemaProperty(key, prop, paths);
  }
  return paths;
}

function walkSchemaProperty(currentPath, prop, paths) {
  paths.push(currentPath);

  if (prop?.type === "object" && isRecord(prop.properties)) {
    for (const [key, child] of Object.entries(prop.properties)) {
      walkSchemaProperty(`${currentPath}.${key}`, child, paths);
    }
  }

  if (prop?.type === "array" && isRecord(prop.items)) {
    paths.push(`${currentPath}[]`);
    if (prop.items.type === "object" && isRecord(prop.items.properties)) {
      for (const [key, child] of Object.entries(prop.items.properties)) {
        walkSchemaProperty(`${currentPath}[].${key}`, child, paths);
      }
    }
  }
}

function joinUrl(baseUrl, path) {
  const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
  return `${baseUrl}/${normalizedPath}`;
}

function isRecord(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

function hasOwn(object, key) {
  return Object.prototype.hasOwnProperty.call(object, key);
}

function isAuthLikeError(message) {
  return (
    message.includes("401") ||
    message.includes("403") ||
    message.includes("X-API-Key") ||
    message.includes("AICADE_GALAXY_API_KEY")
  );
}

function isDebugEnabled(value) {
  if (!value) {
    return false;
  }

  return value.toLowerCase() === "true";
}

function logDebug(config, payload) {
  if (!config.debug) {
    return;
  }

  console.log(`[AICADE_GALAXY_DEBUG] ${JSON.stringify(payload, null, 2)}`);
}

function previewText(text, limit = 240) {
  const compact = text.split(/\s+/).filter(Boolean).join(" ");
  if (compact.length <= limit) {
    return compact;
  }
  return `${compact.slice(0, limit)}...`;
}

function dirname(path) {
  const normalized = path.replace(/\\/g, "/");
  const index = normalized.lastIndexOf("/");
  return index === -1 ? "." : normalized.slice(0, index) || ".";
}

function joinPath(dir, file) {
  const normalizedDir = dir.replace(/[\\/]+$/, "");
  if (!normalizedDir) {
    return file;
  }
  return `${normalizedDir}/${file}`;
}

await main();


path: scripts/invoke_artifact.py
content: #!/usr/bin/env python3

import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Dict, List, Optional


def main() -> None:
    try:
        args = parse_cli_args(sys.argv[1:])
        artifact_path = require_arg(args, "--artifact")
        tool_name = require_arg(args, "--tool")
        args_file = require_arg(args, "--args-file")
        debug = is_debug_enabled(os.environ.get("AICADE_GALAXY_DEBUG"))

        artifact = json.loads(Path(artifact_path).read_text(encoding="utf-8"))
        payload = json.loads(Path(args_file).read_text(encoding="utf-8"))

        if not isinstance(payload, dict):
            raise RuntimeError("--args-file must contain a JSON object.")

        log_debug(
            debug,
            {
                "event": "invoke.input",
                "artifactPath": artifact_path,
                "tool": tool_name,
                "argsFile": args_file,
                "payload": payload,
            },
        )
        result = invoke_artifact_tool(artifact, tool_name, payload)
        log_debug(
            debug,
            {
                "event": "invoke.output",
                "artifactPath": artifact_path,
                "tool": tool_name,
                "result": result,
            },
        )
        print(json.dumps(result, ensure_ascii=False, indent=2))
        sys.exit(0 if result["ok"] else 1)
    except Exception as exc:  # noqa: BLE001
        print(str(exc), file=sys.stderr)
        sys.exit(1)


def invoke_artifact_tool(artifact: dict, tool_name: str, payload: dict) -> dict:
    tool = find_tool(artifact, tool_name)
    split = split_payload(payload)

    validate_args(tool, split["args"])
    validate_response_paths(tool, split["responsePaths"])

    url = join_base_url(artifact["baseUrl"], tool["metadata"]["path"])
    headers = {
        "Accept": "application/json",
    }
    apply_auth_header(tool, headers)

    method = tool["metadata"]["method"]
    body = None
    if method == "GET":
        url = apply_query_params(url, split["args"])
    else:
        headers["Content-Type"] = "application/json"
        body = json.dumps(split["args"]).encode("utf-8")

    request = urllib.request.Request(url, headers=headers, method=method, data=body)

    try:
        with urllib.request.urlopen(request) as response:
            status = response.status
            text = response.read().decode("utf-8", errors="replace")
    except urllib.error.HTTPError as exc:
        status = exc.code
        text = exc.read().decode("utf-8", errors="replace")

    raw = parse_maybe_json(text)
    if status >= 400:
        return {
            "ok": False,
            "status": status,
            "tool": tool["name"],
            "serviceId": tool["metadata"]["serviceId"],
            "error": {
                "message": build_error_message(status, raw),
                "raw": raw,
            },
        }

    return {
        "ok": True,
        "status": status,
        "tool": tool["name"],
        "serviceId": tool["metadata"]["serviceId"],
        "data": select_response_data(raw, split["responsePaths"]),
        "raw": raw,
    }


def parse_cli_args(argv: List[str]) -> Dict[str, str]:
    if len(argv) == 0 or len(argv) % 2 != 0:
        raise RuntimeError(
            "Usage: python3 scripts/invoke_artifact.py --artifact <path> --tool <name> --args-file <path>"
        )

    result: Dict[str, str] = {}
    for index in range(0, len(argv), 2):
        key = argv[index]
        value = argv[index + 1]
        if not key.startswith("--"):
            raise RuntimeError(
                "Usage: python3 scripts/invoke_artifact.py --artifact <path> --tool <name> --args-file <path>"
            )
        result[key] = value
    return result


def require_arg(args: Dict[str, str], key: str) -> str:
    value = args.get(key)
    if not value:
        raise RuntimeError(f"Missing required argument {key}.")
    return value


def find_tool(artifact: dict, tool_name: str) -> dict:
    tools = artifact.get("tools")
    if not isinstance(tools, list):
        raise RuntimeError("Invalid artifact format.")

    for tool in tools:
        if isinstance(tool, dict) and tool.get("name") == tool_name:
            return tool

    raise RuntimeError(f'Tool "{tool_name}" was not found in artifact.')


def split_payload(payload: dict) -> dict:
    args = {key: value for key, value in payload.items() if key != "responsePaths"}
    response_paths = payload.get("responsePaths", [])
    if not isinstance(response_paths, list) or any(
        not isinstance(item, str) or len(item) == 0 for item in response_paths
    ):
        raise RuntimeError('"responsePaths" must be an array of non-empty strings.')
    return {
        "args": args,
        "responsePaths": response_paths,
    }


def validate_args(tool: dict, args: dict) -> None:
    input_schema = tool.get("inputSchema", {})
    required = input_schema.get("required", [])
    properties = input_schema.get("properties", {})

    for key in required:
        if key not in args or args[key] is None:
            raise RuntimeError(f'Missing required parameter "{key}" for tool "{tool["name"]}".')

    if input_schema.get("additionalProperties") is False:
        for key in args:
            if key not in properties:
                raise RuntimeError(f'Unknown parameter "{key}" for tool "{tool["name"]}".')

    for key, value in args.items():
        property_schema = properties.get(key)
        if isinstance(property_schema, dict):
            validate_value_type(tool["name"], key, value, property_schema)


def validate_value_type(tool_name: str, key: str, value, property_schema: dict) -> None:
    expected_type = property_schema.get("type")
    if value is None or not expected_type:
        return

    valid = (
        (expected_type == "string" and isinstance(value, str))
        or (expected_type == "number" and isinstance(value, (int, float)) and not isinstance(value, bool))
        or (expected_type == "integer" and isinstance(value, int) and not isinstance(value, bool))
        or (expected_type == "boolean" and isinstance(value, bool))
        or (expected_type == "object" and isinstance(value, dict))
        or (expected_type == "array" and isinstance(value, list))
    )

    if not valid:
        raise RuntimeError(
            f'Invalid parameter "{key}" for tool "{tool_name}": expected {expected_type}.'
        )


def validate_response_paths(tool: dict, response_paths: List[str]) -> None:
    response_fields = tool.get("metadata", {}).get("responseFields", [])
    if len(response_paths) == 0 or not isinstance(response_fields, list):
        return

    available = set(response_fields)
    for response_path in response_paths:
        if response_path not in available:
            raise RuntimeError(
                f'Unknown response path "{response_path}" for tool "{tool["name"]}".'
            )


def apply_auth_header(tool: dict, headers: Dict[str, str]) -> None:
    auth = tool.get("metadata", {}).get("authentication", {})
    env_name = auth.get("envName")
    header_name = auth.get("headerName")
    required = auth.get("required", False)
    value = os.environ.get(env_name, "") if isinstance(env_name, str) else ""

    if required and not value:
        raise RuntimeError(
            f'Missing required environment variable: {env_name} for tool "{tool["name"]}".'
        )

    if value and isinstance(header_name, str):
        headers[header_name] = value


def apply_query_params(url: str, args: dict) -> str:
    parsed = urllib.parse.urlparse(url)
    existing = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
    extra = [(key, stringify_query_value(value)) for key, value in args.items() if value is not None]
    query = urllib.parse.urlencode(existing + extra)
    return urllib.parse.urlunparse(parsed._replace(query=query))


def stringify_query_value(value) -> str:
    if isinstance(value, (str, int, float, bool)) and not isinstance(value, dict):
        return str(value)
    return json.dumps(value, ensure_ascii=False)


def parse_maybe_json(text: str):
    if len(text) == 0:
        return None
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return text


def build_error_message(status: int, raw) -> str:
    if isinstance(raw, dict):
        if isinstance(raw.get("message"), str) and raw["message"]:
            return raw["message"]
        error = raw.get("error")
        if isinstance(error, dict) and isinstance(error.get("message"), str):
            return error["message"]

    if isinstance(raw, str) and raw:
        return raw

    return f"Request failed with HTTP {status}."


def select_response_data(data, response_paths: List[str]):
    if len(response_paths) == 0:
        return data
    if len(response_paths) == 1:
        return get_value_by_path(data, response_paths[0])

    result = {}
    for path in response_paths:
        result = merge_selected_values(result, build_selected_structure(data, tokenize_path(path)))
    return result


def get_value_by_path(data, path: str):
    return get_value_by_tokens(data, tokenize_path(path))


def build_selected_structure(data, tokens: List[str]):
    if len(tokens) == 0:
        return data

    current = tokens[0]
    rest = tokens[1:]

    if current == "[]":
        if not isinstance(data, list):
            return None
        return [build_selected_structure(item, rest) for item in data]

    if not isinstance(data, dict):
        return None

    selected_child = build_selected_structure(data.get(current), rest)
    if selected_child is None:
        return None
    return {current: selected_child}


def get_value_by_tokens(data, tokens: List[str]):
    if len(tokens) == 0:
        return data

    current = tokens[0]
    rest = tokens[1:]

    if current == "[]":
        if not isinstance(data, list):
            return None
        return [get_value_by_tokens(item, rest) for item in data]

    if not isinstance(data, dict):
        return None

    return get_value_by_tokens(data.get(current), rest)


def tokenize_path(path: str) -> List[str]:
    return [item for item in path.replace("[]", ".[]").split(".") if item]


def merge_selected_values(left, right):
    if right is None:
        return left
    if left is None:
        return right

    if isinstance(left, list) and isinstance(right, list):
        length = max(len(left), len(right))
        return [
            merge_selected_values(
                left[index] if index < len(left) else None,
                right[index] if index < len(right) else None,
            )
            for index in range(length)
        ]

    if isinstance(left, dict) and isinstance(right, dict):
        result = dict(left)
        for key, value in right.items():
            result[key] = (
                merge_selected_values(result.get(key), value)
                if key in result
                else value
            )
        return result

    return right


def join_base_url(base_url: str, path: str) -> str:
    normalized_base = base_url.rstrip("/")
    normalized_path = path if path.startswith("/") else f"/{path}"
    return f"{normalized_base}{normalized_path}"


def is_debug_enabled(value: Optional[str]) -> bool:
    return bool(value) and value.lower() == "true"


def log_debug(enabled: bool, payload: dict) -> None:
    if not enabled:
        return
    print(f"[AICADE_GALAXY_DEBUG] {json.dumps(payload, ensure_ascii=False, indent=2)}")


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)