# Руководство по CLI и SDK

## Обзор

Clore.ai предоставляет **REST API** который обеспечивает полный программный доступ к рынку GPU — просмотр серверов, создание заказов, мониторинг развертываний и отмену аренды.

> **Примечание:** В настоящий момент официального CLI-бинарника нет. Вся автоматизация осуществляется напрямую через REST API с помощью таких инструментов, как `curl`, Python или Node.js.

**Базовый URL:** `https://api.clore.ai/v1`

**Формат ответа:** JSON. Каждый ответ содержит `code` поле, указывающее статус.

***

## Аутентификация

Сгенерируйте ваш API-ключ в [Панели управления Clore.ai](https://clore.ai):

1. Войдите в свою учетную запись
2. Перейдите в **API** раздел в настройках
3. Сгенерируйте и скопируйте ваш API-ключ

**Формат заголовка:**

```
auth: YOUR_API_KEY
```

> ⚠️ **Важно:** Заголовок auth это `auth`, **не** `Authorization: Bearer`. Использование неправильного формата вернет код `3` (Неверный API-токен).

**Пример:**

```bash
curl -H 'auth: YOUR_API_KEY' 'https://api.clore.ai/v1/marketplace'
```

***

## Быстрый старт

### Список серверов на рынке

Просмотрите все доступные GPU-серверы:

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/marketplace'
```

**Ответ:**

```json
{
  "servers": [
    {
      "id": 6,
      "owner": 4,
      "mrl": 73,
      "price": {
        "on_demand": { "bitcoin": 0.00001 },
        "spot": { "bitcoin": 0.000001 }
      },
      "rented": false,
      "specs": {
        "cpu": "Intel Core i9-11900",
        "ram": 62.67,
        "gpu": "1x NVIDIA GeForce GTX 1080 Ti",
        "gpuram": 11,
        "net": { "up": 26.38, "down": 118.42, "cc": "CZ" }
      }
    }
  ],
  "my_servers": [1, 2, 4],
  "code": 0
}
```

***

### Получить ваши заказы

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/my_orders'
```

Включить завершенные/истекшие заказы:

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/my_orders?return_completed=true'
```

***

### Создать заказ (On-Demand)

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "currency": "bitcoin",
    "image": "cloreai/ubuntu20.04-jupyter",
    "renting_server": 6,
    "type": "on-demand",
    "ports": {
      "22": "tcp",
      "8888": "http"
    },
    "ssh_password": "YourSSHPassword123",
    "jupyter_token": "YourJupyterToken123"
  }' \
  'https://api.clore.ai/v1/create_order'
```

**Ответ:**

```json
{ "code": 0 }
```

***

### Создать спотовый заказ

Спотовые заказы дешевле, но могут быть перебиты. Вы указываете вашу цену в день:

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "currency": "bitcoin",
    "image": "cloreai/ubuntu20.04-jupyter",
    "renting_server": 6,
    "type": "spot",
    "spotprice": 0.000005,
    "ports": {
      "22": "tcp",
      "8888": "http"
    },
    "ssh_password": "YourSSHPassword123"
  }' \
  'https://api.clore.ai/v1/create_order'
```

***

### Проверить статус заказа

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/my_orders'
```

Активные заказы содержат `pub_cluster` (имена хостов) и `tcp_ports` для доступа по SSH:

```json
{
  "id": 38,
  "pub_cluster": ["n1.c1.clorecloud.net", "n2.c1.clorecloud.net"],
  "tcp_ports": ["22:10000"],
  "http_port": "8888",
  "expired": false
}
```

Подключение по SSH к арендованному серверу:

```bash
ssh root@n1.c1.clorecloud.net -p 10000
```

***

### Отменить заказ

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "id": 38
  }' \
  'https://api.clore.ai/v1/cancel_order'
```

По желанию сообщите о проблеме с сервером:

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "id": 38,
    "issue": "GPU перегревался и снижал производительность"
  }' \
  'https://api.clore.ai/v1/cancel_order'
```

***

## Python SDK

Легкий обертка, использующая `requests` библиотеку. Установите её с помощью:

```bash
pip install requests
```

### Класс CloreClient

```python
import requests
import time

class CloreClient:
    """Простая Python SDK для Clore.ai REST API."""
    
    BASE_URL = "https://api.clore.ai/v1"
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({"auth": api_key})
    
    def _get(self, endpoint: str, params: dict = None) -> dict:
        url = f"{self.BASE_URL}/{endpoint}"
        response = self.session.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        if data.get("code") != 0:
            raise CloreAPIError(data)
        return data
    
    def _post(self, endpoint: str, body: dict) -> dict:
        url = f"{self.BASE_URL}/{endpoint}"
        self.session.headers.update({"Content-type": "application/json"})
        response = self.session.post(url, json=body)
        response.raise_for_status()
        data = response.json()
        if data.get("code") != 0:
            raise CloreAPIError(data)
        return data
    
    def list_servers(self) -> list:
        """Получить все доступные серверы на рынке."""
        data = self._get("marketplace")
        return data["servers"]
    
    def get_server_details(self, server_id: int) -> dict:
        """Получить информацию о спотовом рынке для конкретного сервера."""
        data = self._get("spot_marketplace", params={"market": server_id})
        return data["market"]
    
    def create_order(
        self,
        server_id: int,
        image: str,
        order_type: str = "on-demand",
        currency: str = "bitcoin",
        spotprice: float = None,
        ports: dict = None,
        ssh_password: str = None,
        ssh_key: str = None,
        jupyter_token: str = None,
        env: dict = None,
        command: str = None,
    ) -> dict:
        """
        Создать on-demand или спотовый заказ.
        
        Аргументы:
            server_id: ID сервера для аренды
            image: Docker-образ (например, 'cloreai/ubuntu20.04-jupyter')
            order_type: 'on-demand' или 'spot'
            currency: 'bitcoin' (по умолчанию)
            spotprice: Обязательно для спотовых заказов — цена в день в BTC
            ports: Проброс портов, например {"22": "tcp", "8888": "http"}
            ssh_password: Пароль SSH (только буквенно-цифровые символы)
            ssh_key: Публичный SSH-ключ
            jupyter_token: Токен для Jupyter notebook
            env: Словарь переменных окружения
            command: Команда оболочки для выполнения после запуска контейнера
        """
        if order_type == "spot" and spotprice is None:
            raise ValueError("spotprice is required for spot orders")
        
        body = {
            "currency": currency,
            "image": image,
            "renting_server": server_id,
            "type": order_type,
        }
        
        if spotprice is not None:
            body["spotprice"] = spotprice
        if ports:
            body["ports"] = ports
        if ssh_password:
            body["ssh_password"] = ssh_password
        if ssh_key:
            body["ssh_key"] = ssh_key
        if jupyter_token:
            body["jupyter_token"] = jupyter_token
        if env:
            body["env"] = env
        if command:
            body["command"] = command
        
        return self._post("create_order", body)
    
    def get_orders(self, include_completed: bool = False) -> list:
        """Получить ваши активные (и опционально завершенные) заказы."""
        params = {"return_completed": "true"} if include_completed else {}
        data = self._get("my_orders", params=params)
        return data["orders"]
    
    def cancel_order(self, order_id: int, issue: str = None) -> dict:
        """Отменить заказ. По желанию сообщить о проблеме."""
        body = {"id": order_id}
        if issue:
            body["issue"] = issue
        return self._post("cancel_order", body)
    
    def get_wallets(self) -> list:
        """Получить ваши кошельки и балансы."""
        data = self._get("wallets")
        return data["wallets"]
    
    def get_my_servers(self) -> list:
        """Получить серверы, которые вы предоставляете на рынок."""
        data = self._get("my_servers")
        return data["servers"]


class CloreAPIError(Exception):
    """Возникает, когда Clore API возвращает код, отличный от нуля."""
    
    ERROR_CODES = {
        0: "Нормально",
        1: "Ошибка базы данных",
        2: "Неверные входные данные",
        3: "Неверный API-токен",
        4: "Неверный endpoint",
        5: "Превышен лимит запросов (1 запрос/сек)",
        6: "Ошибка (см. поле error)",
    }
    
    def __init__(self, response: dict):
        self.code = response.get("code")
        self.error = response.get("error", "")
        message = self.ERROR_CODES.get(self.code, f"Неизвестный код {self.code}")
        if self.error:
            message = f"{message}: {self.error}"
        super().__init__(f"Ошибка Clore API {self.code}: {message}")
```

### Полный рабочий пример

```python
import os
from clore_client import CloreClient, CloreAPIError

# Инициализация клиента
client = CloreClient(api_key=os.environ["CLORE_API_KEY"])

# 1. Просмотреть рынок
servers = client.list_servers()
print(f"Найдено {len(servers)} серверов на рынке")

# 2. Отфильтровать доступные серверы RTX 4090
rtx4090_servers = [
    s for s in servers
    if "4090" in s["specs"].get("gpu", "") and not s["rented"]
]

if not rtx4090_servers:
    print("Не найдено доступных серверов RTX 4090")
    exit(1)

# 3. Выбрать самый дешевый
cheapest = min(rtx4090_servers, key=lambda s: s["price"]["on_demand"]["bitcoin"])
print(f"Самый дешевый RTX 4090: ID сервера {cheapest['id']}, "
      f"цена {cheapest['price']['on_demand']['bitcoin']:.8f} BTC/день")

# 4. Создать заказ
try:
    client.create_order(
        server_id=cheapest["id"],
        image="cloreai/ubuntu20.04-jupyter",
        order_type="on-demand",
        currency="bitcoin",
        ports={"22": "tcp", "8888": "http"},
        ssh_password="SecurePass123",
        jupyter_token="MyToken123",
    )
    print("Заказ успешно создан!")
except CloreAPIError as e:
    print(f"Не удалось создать заказ: {e}")
    exit(1)

# 5. Проверить ваши заказы
orders = client.get_orders()
for order in orders:
    if not order.get("expired"):
        cluster = order.get("pub_cluster", [])
        tcp = order.get("tcp_ports", [])
        print(f"Заказ {order['id']}: сервер {order['si']}")
        if cluster and tcp:
            ssh_port = tcp[0].split(":")[1]
            print(f"  SSH: ssh root@{cluster[0]} -p {ssh_port}")
```

***

## Пример Node.js

Использование нативного `fetch` API (Node.js 18+):

```javascript
const BASE_URL = 'https://api.clore.ai/v1';

class CloreClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.defaultHeaders = {
      'auth': apiKey,
    };
  }

  async get(endpoint, params = {}) {
    const url = new URL(`${BASE_URL}/${endpoint}`);
    Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
    
    const res = await fetch(url.toString(), {
      headers: this.defaultHeaders,
    });
    
    const data = await res.json();
    if (data.code !== 0) {
      throw new Error(`Clore API error ${data.code}: ${data.error || 'unknown'}`);
    }
    return data;
  }

  async post(endpoint, body) {
    const res = await fetch(`${BASE_URL}/${endpoint}`, {
      method: 'POST',
      headers: {
        ...this.defaultHeaders,
        'Content-type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    
    const data = await res.json();
    if (data.code !== 0) {
      throw new Error(`Clore API error ${data.code}: ${data.error || 'unknown'}`);
    }
    return data;
  }

  async listServers() {
    const data = await this.get('marketplace');
    return data.servers;
  }

  async getOrders(includeCompleted = false) {
    const params = includeCompleted ? { return_completed: 'true' } : {};
    const data = await this.get('my_orders', params);
    return data.orders;
  }

  async createOrder({ serverId, image, type = 'on-demand', currency = 'bitcoin', spotprice, ports, sshPassword, jupyterToken, env, command }) {
    const body = {
      currency,
      image,
      renting_server: serverId,
      type,
      ...(spotprice && { spotprice }),
      ...(ports && { ports }),
      ...(sshPassword && { ssh_password: sshPassword }),
      ...(jupyterToken && { jupyter_token: jupyterToken }),
      ...(env && { env }),
      ...(command && { command }),
    };
    return this.post('create_order', body);
  }

  async cancelOrder(orderId, issue = null) {
    const body = { id: orderId };
    if (issue) body.issue = issue;
    return this.post('cancel_order', body);
  }
}

// Пример использования
const client = new CloreClient(process.env.CLORE_API_KEY);

async function main() {
  // Список на рынке
  const servers = await client.listServers();
  console.log(`На рынке ${servers.length} серверов`);

  // Найти доступные серверы RTX 4090
  const available = servers.filter(
    s => s.specs.gpu.includes('4090') && !s.rented
  );
  console.log(`Доступных серверов RTX 4090: ${available.length}`);

  // Получить текущие заказы
  const orders = await client.getOrders();
  const activeOrders = orders.filter(o => !o.expired);
  console.log(`Активных заказов: ${activeOrders.length}`);

  for (const order of activeOrders) {
    const host = order.pub_cluster?.[0];
    const portMapping = order.tcp_ports?.[0];
    if (host && portMapping) {
      const sshPort = portMapping.split(':')[1];
      console.log(`Заказ ${order.id} → ssh root@${host} -p ${sshPort}`);
    }
  }
}

main().catch(console.error);
```

***

## Распространенные рабочие сценарии

### Найти самый дешевый RTX 4090 и арендовать его

```python
from clore_client import CloreClient

client = CloreClient(api_key="YOUR_API_KEY")

def rent_cheapest_rtx4090(image="cloreai/ubuntu20.04-jupyter", ssh_password="SecurePass123"):
    servers = client.list_servers()
    
    # Фильтр: доступный RTX 4090
    candidates = [
        s for s in servers
        if "4090" in s["specs"].get("gpu", "")
        and not s["rented"]
    ]
    
    if not candidates:
        raise RuntimeError("Не найдено доступных серверов RTX 4090")
    
    # Сортировать по цене on-demand в BTC
    candidates.sort(key=lambda s: s["price"]["on_demand"]["bitcoin"])
    best = candidates[0]
    
    price_btc = best["price"]["on_demand"]["bitcoin"]
    print(f"Аренда сервера {best['id']}: {best['specs']['gpu']} @ {price_btc:.8f} BTC/день")
    
    client.create_order(
        server_id=best["id"],
        image=image,
        order_type="on-demand",
        currency="bitcoin",
        ports={"22": "tcp"},
        ssh_password=ssh_password,
    )
    
    print("Готово! Проверьте ваши заказы для деталей подключения по SSH.")
    return best["id"]

rent_cheapest_rtx4090()
```

***

### Мониторинг моих заказов

```python
import time
from clore_client import CloreClient

client = CloreClient(api_key="YOUR_API_KEY")

def monitor_orders(poll_interval_seconds=60):
    """Опрос заказов и вывод обновлений статуса."""
    print(f"Мониторинг заказов (опрос каждые {poll_interval_seconds}s). Ctrl+C чтобы остановить.\n")
    
    while True:
        orders = client.get_orders(include_completed=False)
        active = [o for o in orders if not o.get("expired")]
        
        print(f"--- {len(active)} активных заказ(а/ов) ---")
        for order in active:
            cluster = order.get("pub_cluster", [])
            tcp = order.get("tcp_ports", [])
            spend = order.get("spend", 0)
            
            ssh_info = ""
            if cluster and tcp:
                port = tcp[0].split(":")[1]
                ssh_info = f" | SSH: {cluster[0]}:{port}"
            
            print(f"  Заказ {order['id']}: сервер {order['si']}"
                  f" | потрачено {spend:.8f} BTC{ssh_info}")
        
        if not active:
            print("  Нет активных заказов.")
        
        print()
        time.sleep(poll_interval_seconds)

monitor_orders()
```

***

### Авто-аренда при падении цены ниже X

```python
import time
from clore_client import CloreClient

client = CloreClient(api_key="YOUR_API_KEY")

def auto_rent_on_price_drop(
    gpu_model: str = "RTX 4090",
    max_price_btc: float = 0.00015,
    image: str = "cloreai/ubuntu20.04-jupyter",
    ssh_password: str = "SecurePass123",
    check_interval_seconds: int = 120,
):
    """
    Следить за рынком и автоматически арендовать GPU, когда цена опускается ниже порога.
    
    Аргументы:
        gpu_model: Название GPU для поиска (без учета регистра)
        max_price_btc: Максимально допустимая цена в день в BTC
        image: Docker-образ для развертывания
        ssh_password: Пароль SSH для контейнера
        check_interval_seconds: Как часто проверять (уважайте лимиты запросов!)
    """
    print(f"Отслеживание {gpu_model} при цене ≤ {max_price_btc:.8f} BTC/день...")
    
    while True:
        servers = client.list_servers()
        
        for server in servers:
            gpu = server["specs"].get("gpu", "")
            if gpu_model.lower() not in gpu.lower():
                continue
            if server["rented"]:
                continue
            
            price = server["price"]["on_demand"]["bitcoin"]
            if price <= max_price_btc:
                print(f"🎯 Найдена подходящая машина! Сервер {server['id']}: {gpu} @ {price:.8f} BTC/день")
                
                try:
                    client.create_order(
                        server_id=server["id"],
                        image=image,
                        order_type="on-demand",
                        currency="bitcoin",
                        ports={"22": "tcp"},
                        ssh_password=ssh_password,
                        required_price=price,  # Зафиксировать эту цену
                    )
                    print(f"✅ Заказ создан для сервера {server['id']}!")
                    return server["id"]
                except Exception as e:
                    print(f"Не удалось создать заказ: {e}. Попробую снова...")
        
        print(f"Совпадения пока нет. Проверим снова через {check_interval_seconds}с...")
        time.sleep(check_interval_seconds)

auto_rent_on_price_drop(gpu_model="4090", max_price_btc=0.00012)
```

***

## WebSocket

REST API Clore.ai в настоящее время не предоставляет конечную точку WebSocket. Для мониторинга в реальном времени используйте опрос с разумным интервалом (см. лимиты скорости ниже).

***

## Лимиты скорости

| Конечная точка                   | Лимит                          |
| -------------------------------- | ------------------------------ |
| Большинство конечных точек       | **1 запрос/секунда**           |
| `create_order`                   | **1 запрос/5 секунд**          |
| `set_spot_price` (снижение цены) | Один раз каждые **600 секунд** |

**Ответ при превышении лимита (код 5):**

```json
{ "code": 5 }
```

**Рекомендуемые практики:**

* Добавьте `time.sleep(1)` между последовательными вызовами API
* Для `create_order`, подождите по крайней мере 5 секунд между запросами
* Используйте интервалы опроса 60+ секунд для циклов мониторинга
* Кэшируйте данные маркетплейса локально, если вам нужно часто их запрашивать

**Помощник на Python:**

```python
import time

def safe_api_call(fn, *args, delay=1.1, **kwargs):
    """Обёртка для вызова API с защитой от превышения лимитов."""
    result = fn(*args, **kwargs)
    time.sleep(delay)
    return result
```

***

## Обработка ошибок

Каждый ответ API включает поле `code` поле. Значение `0` означает успех.

### Коды ошибок

| Код | Значение                               | Действие                                   |
| --- | -------------------------------------- | ------------------------------------------ |
| `0` | Успех                                  | —                                          |
| `1` | Ошибка базы данных                     | Повторите через некоторое время            |
| `2` | Неверные входные данные                | Проверьте тело запроса/параметры           |
| `3` | Неверный API токен                     | Проверьте ваш API-ключ в панели управления |
| `4` | Неверная конечная точка                | Проверьте URL конечной точки               |
| `5` | Превышен лимит запросов (1 запрос/сек) | Добавьте задержки между запросами          |
| `6` | Ошибка приложения (см. `ошибка` поле)  | Прочитайте `ошибка` поле для подробностей  |

### Подошибки кода 6

| `ошибка` значение             | Значение                                                                              |
| ----------------------------- | ------------------------------------------------------------------------------------- |
| `exceeded_max_step`           | Снижение спотовой цены слишком велико; проверьте `max_step` поле                      |
| `can_lower_every_600_seconds` | Нужно подождать перед следующим снижением спотовой цены; проверьте `time_to_lowering` |

### Пример обработки ошибок в Python

```python
from clore_client import CloreClient, CloreAPIError
import time

client = CloreClient(api_key="YOUR_API_KEY")

def create_order_with_retry(server_id, image, max_retries=3):
    for attempt in range(max_retries):
        try:
            return client.create_order(
                server_id=server_id,
                image=image,
                order_type="on-demand",
                currency="bitcoin",
                ports={"22": "tcp"},
                ssh_password="SecurePass123",
            )
        except CloreAPIError as e:
            if e.code == 5:  # Rate limit
                print(f"Превышен лимит. Ждём 5с... (попытка {attempt+1}/{max_retries})")
                time.sleep(5)
            elif e.code == 3:  # Bad API key
                print("Неверный API ключ! Проверьте ваш CLORE_API_KEY.")
                raise
            elif e.code == 2:  # Bad input
                print(f"Неверный запрос: {e}")
                raise
            else:
                print(f"Ошибка API: {e}. Повтор через 3с...")
                time.sleep(3)
    
    raise RuntimeError(f"Не удалось после {max_retries} попыток")
```

### Пример обработки ошибок в JavaScript

```javascript
async function createOrderWithRetry(client, serverConfig, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.createOrder(serverConfig);
    } catch (err) {
      const code = parseInt(err.message.match(/error (\d+)/)?.[1]);
      
      if (code === 5) {
        console.log(`Превышен лимит. Ждём 5с... (попытка ${attempt + 1}/${maxRetries})`);
        await new Promise(r => setTimeout(r, 5000));
      } else if (code === 3) {
        throw new Error('Неверный API ключ');
      } else {
        console.log(`Ошибка API: ${err.message}. Повтор через 3с...`);
        await new Promise(r => setTimeout(r, 3000));
      }
    }
  }
  throw new Error(`Не удалось после ${maxRetries} попыток`);
}
```

***

## Доступные Docker-образы

Clore.ai предоставляет предварительно собранные образы, оптимизированные для задач на GPU:

| Образ                         | Описание                  |
| ----------------------------- | ------------------------- |
| `cloreai/ubuntu20.04-jupyter` | Ubuntu 20.04 + JupyterLab |
| `cloreai/ubuntu22.04-jupyter` | Ubuntu 22.04 + JupyterLab |

Вы также можете использовать любой публичный образ с Docker Hub. Для доступа к GPU используйте образы с поддержкой CUDA, например:

```
nvidia/cuda:12.1.0-devel-ubuntu22.04
```

***

## Проброс портов

При создании заказа укажите порты для экспорта:

```json
{
  "ports": {
    "22": "tcp",
    "8888": "http",
    "6006": "http"
  }
}
```

* **`"tcp"`** — Прямой проброс TCP-порта (для SSH, пользовательских серверов)
* **`"http"`** — HTTPS-прокси (для Jupyter, веб-интерфейсов). Разрешён только один HTTP-порт на `http_port` поле.

После создания заказа данные для подключения появятся в `my_orders`:

```json
{
  "pub_cluster": ["n1.c1.clorecloud.net", "n2.c1.clorecloud.net"],
  "tcp_ports": ["22:10000"],
  "http_port": "8888"
}
```

Подключиться через SSH:

```bash
ssh root@n1.c1.clorecloud.net -p 10000
```

Доступ к Jupyter через браузер: `https://n1.c1.clorecloud.net` (использует `http_port`)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.clore.ai/clore.ai/clore.ai-ru/razrabotchiki/cli-sdk.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
