---
title: "Як створити CLI-інструмент на PHP для AI-агентів і запускати його будь-де через CPX"
url: https://laravelukraine.com/blog/iak-stvoriti-cli-instrument-na-php-dlia-ai-agentiv-i-zapuskati-iogo-bud-de-cerez-cpx
date: 2026-11-09
source: https://tighten.com/insights/build-an-agent-friendly-cli-tool-in-php-and-run-it-anywhere-with-cpx/
---

# Як створити CLI-інструмент на PHP для AI-агентів і запускати його будь-де через CPX

> **Від редакції.** Ми вже [писали про CPX](https://laravelukraine.com/blog/cpx-instrument-dlia-vikonannia-composer-paketiv-bez-vstanovlennia), коли це був сторонній проєкт. З того часу CPX став офіційним пакетом Laravel: версію 2.0 анонсували на Laracon US 2026. Ця стаття від Tighten показує його в ділі.

Інструменти командного рядка популярні як ніколи. Вони корисні не лише людям, а й агентам: консольна команда, яка повертає дані в JSON, - це саме те, що потрібно будь-якому AI-агенту.

Сьогодні ми навчимося створювати такий інструмент і публікувати його на Packagist. Завдяки CPX будь-хто зможе запустити його з будь-якого каталогу без встановлення глобальних залежностей:

```bash
cpx nicodevs/weather "Tokyo"
```

```json
{
  "city": "Tokyo, Japan",
  "temperature": 21.4,
  "wind": 12.3
}
```

Стоп, CPX? Що це таке?

`cpx` для Composer - те саме, що `npx` для npm. Назва розшифровується як Composer Package Executor: він дає змогу запускати команди будь-якого Composer-пакета, не додаючи його ні до проєкту, ні до глобальних залежностей.

Розберімося, на що здатен CPX, і заразом побудуймо свій CLI-інструмент.

### Проблема глобальних пакетів

Знаю, про що ви зараз думаєте:

Запускати Composer-пакети з будь-якого каталогу можна й так. Просто встановіть пакет глобально!

Так, `composer global require`. Саме так ми й робили багато років. Але в цього способу є кілька недоліків, і CPX покликаний їх усунути.

#### Конфлікти залежностей

Composer ставиться до глобального встановлення як до будь-якого іншого проєкту. У нього є власний `composer.json` і власний каталог `vendor`, і кожен інструмент, який ви встановлюєте глобально, потрапляє в той самий набір залежностей. Отже, усі ці інструменти мають погодитися на одну версію кожної спільної бібліотеки.

Наприклад, два різні глобально встановлені пакети можуть використовувати `symfony/console`. Щойно один вимагає `symfony/console` 6, а інший - 7, жодна версія не задовольняє обидва, і Composer видає таку помилку:

```text
Your requirements could not be resolved to an installable set of packages.
```

#### Глобальні оновлення

Глобальні пакети не оновлюються самі. Інструмент, який ви встановили два роки тому, досі тієї самої версії, якщо ви не згадали запустити `composer global update`. Коли ви робили це востаннє? Ага, я теж.

### CPX поспішає на допомогу

CPX працює інакше: замість того щоб встановлювати залежності в спільний глобальний каталог, він дає кожному пакету окреме встановлення в `~/.cpx`. Кожен пакет розв'язує залежності сам, у власному каталозі `vendor`. Два інструменти можуть залежати від різних версій однієї бібліотеки й ніколи не перетнутися.

CPX перевіряє оновлення своїх ізольованих пакетів під час запуску, щонайбільше раз на годину. Якщо є новіша версія, яка відповідає вашому обмеженню версії, він оновлює пакет і потім виконує команду.

CPX створив Ліам Гаммет (Liam Hammett) у 2024 році. У липні 2026 року команда Laravel переписала його з нуля й випустила CPX 2.0 як офіційний пакет Laravel. Тейлор Отвелл представив його у своїй [доповіді](https://www.youtube.com/watch?v=92Xn3NqPGlw&t=1190s) на Laracon US 2026.

### Встановлення CPX

Для самого CPX потрібен PHP 8.3 або новіший. Інструмент, який ми збудуємо далі, використовує можливість PHP 8.5, тож якщо плануєте повторювати кроки за статтею, вам знадобиться саме 8.5. Встановіть CPX через Composer такою командою:

```bash
composer global require cpx/cpx
```

Стоп, `composer global`? Хіба це не та сама річ, яку ми щойно назвали проблемою? Що ж, CPX - єдиний виняток. Він постачається як самодостатній PHAR, усі залежності якого зібрані всередині, тож він нічого не додає до вашого глобального набору залежностей і не може ні з чим там конфліктувати.

Переконайтеся, що глобальний каталог `bin` Composer є у вашому `PATH`. Якщо не знаєте, де він, ця команда підкаже:

```bash
composer global config bin-dir --absolute
```

От і все! Тепер `cpx` можна запускати звідусіль.

### Запуск пакета

Почнімо з найпростішого прикладу. Припустімо, ви хочете запустити [PHP Insights](https://github.com/nunomaduro/phpinsights) на проєкті, але його не встановлено ні в проєкті, ні глобально. З CPX достатньо виконати:

```bash
cpx nunomaduro/phpinsights
```

Під час першого запуску CPX завантажує пакет, ізольовано встановлює його залежності й запускає інструмент для поточного каталогу. Це може тривати кілька секунд. Наступні запуски використовують це встановлення повторно, якщо не настав час оновлення.

Загальний вигляд команди CPX - назва пакета, потім команда, потім її аргументи:

```bash
cpx <vendor>/<package> <command> [arguments]
```

Наприклад, запуск PHP Insights для одного каталогу в повній формі виглядає так. Перша частина визначає пакет, друга - виконуваний файл (binary) усередині нього, а решта - те, що ви зазвичай передали б інструменту:

```bash
cpx nunomaduro/phpinsights phpinsights analyse ./src
```

Коли в пакеті лише один виконуваний файл або він має ту саму назву, що й пакет, другу частину можна опустити. PHP Insights відповідає обом умовам, тож спрацює така команда:

```bash
cpx nunomaduro/phpinsights analyse ./src
```

Можна також вказати конкретну версію з тим самим синтаксисом обмежень, що й у `composer.json`:

```bash
cpx nunomaduro/phpinsights:^2.0 analyse ./src
```

У будь-якому разі `composer.json` вашого проєкту не змінюється. Якщо інструмент уже встановлено в проєкті, CPX запускає саме цю копію.

#### Створення псевдонімів

Щоразу набирати повну назву пакета трохи втомлює. На щастя, CPX дає змогу визначати псевдоніми командою `alias`:

```bash
cpx alias nunomaduro/phpinsights insights
cpx alias laravel/installer laravel
cpx alias laravel/pint
```

Якщо не вказати назву псевдоніма, CPX використовує коротку назву пакета, тож остання команда створює псевдонім `pint`. Щоб видалити псевдонім, виконайте:

```bash
cpx unalias laravel
```

Псевдоніми зберігаються в `~/.cpx/`, поруч зі встановленими пакетами.

#### Керування пакетами

З часом у `~/.cpx` накопичиться кілька пакетів. Для керування ними CPX має кілька команд:

```bash
cpx installed       # every package you've run, with the last time you ran it
cpx update          # update all packages within their constraints
cpx update laravel  # or only the ones from a vendor
cpx clean           # remove the packages you haven't used in a while
cpx clean --all     # remove everything
```

Тут `cpx installed` показує всі пакети, які ви запускали, і коли востаннє; `cpx update` оновлює всі пакети в межах їхніх обмежень версій (або лише пакети одного вендора); `cpx clean` видаляє пакети, якими ви давно не користувалися, а `cpx clean --all` - усе.

`cpx update` знадобиться рідко, адже CPX періодично перевіряє оновлення, коли ви запускаєте пакет. Проте команда стане в пригоді, коли хочеться оновити все й одразу.

Щоб оновити сам CPX, виконайте `composer global update cpx/cpx`, якщо встановлювали його через Composer, або `cpx self-update`, якщо користуєтеся окремим PHAR.

### Будуємо власний CLI-інструмент для CPX

Час виконати обіцянку зі вступу.

Ми збудуємо `weather` - інструмент, який приймає назву міста й виводить поточну погоду в JSON, - і опублікуємо його, щоб будь-хто міг запустити його командою `cpx nicodevs/weather "Tokyo"`. Знаю, приклад штучний! Сміливо замінюйте логіку на ту, яка потрібна вам. Головне тут - зрозуміти, як структурувати й публікувати пакет.

Почнімо з найпростішої версії. Скористаємося [Open-Meteo](https://open-meteo.com/) - API погоди, безкоштовним для некомерційного використання, якому для нашого прикладу не потрібен API-ключ. Звернемося до ендпоїнта прогнозу через `file_get_contents` із жорстко заданим містом. За замовчуванням температура повертається в градусах Цельсія, а швидкість вітру - в км/год.

У порожньому каталозі створіть `weather.php` і вставте такий код:

```php
<?php

$url = 'https://api.open-meteo.com/v1/forecast?' . http_build_query([
  'latitude' => 35.6895,
  'longitude' => 139.6917,
  'current' => 'temperature_2m,wind_speed_10m',
]);

$data = json_decode(file_get_contents($url), true);

echo json_encode([
  'city' => 'Tokyo',
  'temperature' => $data['current']['temperature_2m'],
  'wind' => $data['current']['wind_speed_10m'],
], JSON_PRETTY_PRINT) . PHP_EOL;
```

Щоб виконати код, запустіть у терміналі `php weather.php`. Ви маєте побачити такий результат:

```json
{
  "city": "Tokyo",
  "temperature": 21,
  "wind": 0.4
}
```

Поки що все добре! Перш ніж іти далі, перетворімо цей скрипт на виконуваний файл. Додайте shebang найпершим рядком `weather.php`, перед тегом `<?php`:

```php
#!/usr/bin/env php
<?php
```

Shebang каже операційній системі виконувати цей файл через PHP, тож його можна запускати напряму, не пишучи `php` попереду. Потім перенесіть файл у каталог `bin` і зробіть його виконуваним. Каталог `bin` зазвичай містить готові до запуску програми, і у вас він, можливо, уже є. Якщо ні, спершу створіть його:

```bash
mkdir bin
mv weather.php bin/weather
chmod +x bin/weather
```

Відтепер інструмент запускається як `bin/weather`. Пізніше, коли публікуватимемо його, ми скажемо Composer (і CPX), що цей файл - наш виконуваний файл.

#### Клас OpenMeteo

Інкапсулюймо виклики з поточної чернетки в повноцінний клас `OpenMeteo`, щоб він міг приймати місто як параметр. Далі ми використовуватимемо оператор конвеєра (pipe operator) з PHP 8.5, тож переконайтеся, що у вас PHP 8.5 або новіший.

Створіть `src/OpenMeteo.php`:

```php
<?php

namespace Nicodevs\Weather;

use RuntimeException;

class OpenMeteo
{
  public function currentConditions(string $city): array
  {
    $place = $this->geocode($city);

    $data = $this->get('https://api.open-meteo.com/v1/forecast', [
      'latitude' => $place['latitude'],
      'longitude' => $place['longitude'],
      'current' => 'temperature_2m,wind_speed_10m',
    ]);

    return [
      'city' => "{$place['name']}, {$place['country']}",
      'temperature' => $data['current']['temperature_2m'],
      'wind' => $data['current']['wind_speed_10m'],
    ];
  }

  private function geocode(string $city): array
  {
    $places = $this->get('https://geocoding-api.open-meteo.com/v1/search', ['name' => $city, 'count' => 1]);

    return $places['results'][0] ?? throw new RuntimeException("Could not find a city called {$city}");
  }

  private function get(string $url, array $query): array
  {
    $context = stream_context_create(['http' => ['timeout' => 10]]);

    return $url . '?' . http_build_query($query)
      |> (fn ($url) => file_get_contents($url, false, $context))
      |> (fn ($json) => json_decode($json, true, flags: JSON_THROW_ON_ERROR));
  }
}
```

Навіщо нам клас? Бо виконуваний файл має лишатися легким, а логіку роботи з API має бути можливо тестувати без командного рядка. Усе, що спілкується з Open-Meteo, житиме в класі `OpenMeteo`, а все, що спілкується з терміналом, - у `bin/weather`.

Розберімо код:

- `currentConditions` - це той самий запит, що й раніше, тільки координати тепер беруться з `geocode`, а не задані жорстко. Це єдиний публічний метод, бо зовнішньому світу більше нічого й не треба.
- `geocode` отримує рядок із назвою міста й повертає найкращий збіг із координатами, потрібними для ендпоїнта прогнозу.
- `get` робить те саме, що й перший скрипт, але ланцюжком: будує URL, завантажує його й декодує JSON. Саме для цього в PHP 8.5 з'явився оператор конвеєра `|>`: він передає значення зліва як аргумент колбеку справа, тож кроки читаються згори донизу, а не зсередини назовні.

Запит має десятисекундний тайм-аут, а некоректний JSON викине виняток. PHP-попередження ми обробимо нижче, у точці входу CLI.

Щоб використати клас у скрипті, доручимо його автозавантаження Composer. Створіть файл `composer.json`, який зіставляє наш простір імен із каталогом `src/`:

```json
{
  "autoload": {
    "psr-4": {
      "Nicodevs\\Weather\\": "src/"
    }
  }
}
```

Потім виконайте `composer dump-autoload`, щоб згенерувати автозавантажувач.

Відтепер будь-який клас у просторі імен `Nicodevs\Weather` знаходиться автоматично, якщо виконуваний файл підключає `vendor/autoload.php`.

#### Читання параметрів

Наш інструмент має отримувати місто як параметр, і для цього ми скористаємося пакетом `symfony/console`. Виконайте в терміналі:

```bash
composer require symfony/console
```

Консоль Symfony складається з двох будівельних блоків:

- `Command` оголошує аргументи й опції та містить код, який треба виконати.
- `Application` збирає команди й спрямовує вхідні дані до потрібної команди.

У нас лише одна команда, тож зареєструємо її як команду за замовчуванням. Так інструмент запускатиметься як `weather Tokyo`, без назви команди посередині. Сама команда - це один ланцюжок конвеєра: беремо місто, шукаємо його, кодуємо результат, виводимо. Ось новий код `bin/weather`:

```php
#!/usr/bin/env php
<?php

use Nicodevs\Weather\OpenMeteo;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

require __DIR__ . '/../vendor/autoload.php';

$command = (new Command('weather'))
  ->addArgument('city', InputArgument::REQUIRED, 'City to look up')
  ->setCode(function (InputInterface $input, OutputInterface $output): int {
    $input->getArgument('city')
      |> (new OpenMeteo())->currentConditions(...)
      |> (fn ($weather) => json_encode($weather, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR))
      |> $output->writeln(...);

    return 0;
  });

$app = new Application('weather', '0.1.0');
$app->addCommand($command);
$app->setDefaultCommand('weather', true);
$app->setCatchExceptions(false);

set_error_handler(
  fn ($severity, $message) => throw new ErrorException($message, 0, $severity),
  E_WARNING,
);

try {
  exit($app->run());
} catch (Throwable $e) {
  echo json_encode(['error' => $e->getMessage()], JSON_INVALID_UTF8_SUBSTITUTE) . PHP_EOL;
  exit(1);
}
```

Найважливіший тут останній блок. За замовчуванням Symfony перехоплює кожен виняток і виводить гарно відформатоване текстове повідомлення. Для людей це зручно й читабельно, але ми хочемо отримувати і результати, і оброблені помилки в JSON.

Тому ми вимикаємо цю поведінку через `setCatchExceptions(false)` і обгортаємо `run()` власним `try/catch`. Після цієї зміни кожен виняток із `run()` потрапляє в одне місце - байдуже, чи кинув його наш клас `OpenMeteo`, чи Symfony скаржиться на відсутній аргумент. Ми виводимо його як JSON-помилку й завершуємо роботу з кодом 1. `json_encode` бере на себе екранування, а `JSON_INVALID_UTF8_SUBSTITUTE` дає змогу закодувати повідомлення про помилку навіть із некоректними вхідними даними. Саме за кодом виходу скрипт (чи агент) дізнається, що команда не вдалася.

Обробник помилок перетворює PHP-попередження, як-от невдалі запити чи відсутні поля у відповіді, на винятки, тож вони потрапляють у той самий JSON-обробник помилок.

Що ж, спробуймо:

```bash
bin/weather "New York"
```

```json
{
  "city": "New York, United States",
  "temperature": 16.5,
  "wind": 7.9
}
```

Чудово! Тепер спробуймо місто, якого не існує, наприклад Fooville. Для назви з одного слова лапки можна опустити.

```bash
bin/weather Fooville
```

```json
{"error":"Could not find a city called Fooville"}
```

Без міста взагалі:

```bash
bin/weather
```

```json
{"error":"Not enough arguments (missing: \"city\")."}
```

І з неіснуючою опцією:

```bash
bin/weather "Tokyo" --bogus
```

```json
{"error":"The \"--bogus\" option does not exist."}
```

Крім того, `bin/weather --help` показує довідку з переліком аргументів:

```text
Usage:
  weather <city>

Arguments:
  city                  City to look up
```

Результати й оброблені помилки приходять у JSON. Винятком є стандартні прапорці CLI: `--help` і `--version` виводять текст, а `--quiet` приховує вивід у разі успіху.

### Публікація пакета

Наш інструмент працює, але існує лише на нашій машині.

Щоб опублікувати його, потрібні три речі: назва пакета, запис у `composer.json`, який підкаже Composer (і CPX), де лежить виконуваний файл, і автозавантажувач, який працює незалежно від того, куди встановлено пакет.

#### Composer JSON

Відкрийте `composer.json` і додайте назву, опис, тип, ліцензію, версію PHP і ключ `bin`:

```json
{
  "name": "nicodevs/weather",
  "description": "Weather for any city, as JSON.",
  "type": "library",
  "license": "MIT",
  "bin": ["bin/weather"],
  "require": {
    "php": "^8.5",
    "symfony/console": "^8.0"
  },
  "autoload": {
    "psr-4": {
      "Nicodevs\\Weather\\": "src/"
    }
  }
}
```

Тут важливі два ключі:

- `name` - це те, що люди писатимуть після `cpx`.
- `bin` - те, що перетворює пакет на інструмент командного рядка. Він каже Composer відкрити `bin/weather` як виконуваний файл, а CPX - який файл запускати.

Оскільки `weather` - єдиний виконуваний файл і його назва збігається з назвою пакета, CPX дозволяє пропустити назву команди. Тому `cpx nicodevs/weather "Tokyo"` - це все, що потрібно.

#### Шлях до автозавантажувача

У `bin/weather` лишилася ще одна деталь, яку треба змінити. Зараз він підключає `__DIR__ . '/../vendor/autoload.php'`, і це правильно, коли інструмент запускається з власного каталогу. Але щойно пакет встановлено як залежність, він лежить у `vendor/nicodevs/weather`, а автозавантажувач проєкту - на три рівні вище. Саме так CPX встановлює пакети, тож виконуваний файл має перевіряти обидва місця. Оновіть `bin/weather` так:

```php
require file_exists(__DIR__ . '/../vendor/autoload.php')
  ? __DIR__ . '/../vendor/autoload.php'
  : __DIR__ . '/../../../autoload.php';
```

Це єдина зміна. Пакет готовий:

```text
weather/
├── bin/
│   └── weather
├── src/
│   └── OpenMeteo.php
├── composer.json
└── vendor/
```

#### Перевіряємо з CPX

Ми ще нічого не опублікували, але CPX уміє запускати пакет просто з локального каталогу. Зсередини нашого каталогу передайте `.` як пакет:

```bash
cpx . "Tokyo"
```

```json
{
  "city": "Tokyo, Japan",
  "temperature": 21.6,
  "wind": 2.1
}
```

CPX читає запис `bin` із `composer.json` і запускає виконуваний файл, нічого не копіюючи й не кешуючи. Щоб запустити пакет з іншого місця, передайте замість крапки шлях до каталогу.

Навіщо це, якщо `bin/weather` і так працює? Бо так пакет запускається саме тим способом, яким CPX запускатиме його після публікації: читає `composer.json`, знаходить запис `bin` і запускає файл звідти. Якщо в метаданих щось не так, ви дізнаєтеся про це тут, а не після першого випуску.

#### Packagist

Завантажте пакет у публічний репозиторій на GitHub і позначте тегом перший випуск:

```bash
echo "vendor/" > .gitignore
git init -b main && git add . && git commit -m "first release"
git remote add origin git@github.com:nicodevs/weather.git
git push -u origin main
git tag v0.1.0 && git push --tags
```

Потім відкрийте [packagist.org/packages/submit](https://packagist.org/packages/submit), вставте URL репозиторію й натисніть Submit.

Packagist прочитає `composer.json`, підхопить тег, і за хвилину пакет буде доступний. Якщо ввімкнути інтеграцію з GitHub, яку Packagist пропонує на сторінці пакета, нові теги з'являтимуться самі.

От і все! Пакет можна запускати на будь-якій машині з PHP і CPX:

```bash
cpx nicodevs/weather "Tokyo"
```

Перший запуск ізольовано встановлює пакет у `~/.cpx`. Наступні запуски використовують це встановлення з періодичними оновленнями. Жодного `composer global require`, жодних конфліктів залежностей, жодних ручних оновлень, про які треба пам'ятати.

### GitHub Action для публікації нових версій

Було б зручно автоматично публікувати нову версію пакета щоразу, коли ми пушимо в `main`, включно зі злиттями.

Packagist уже стежить за новими тегами в репозиторії, тож «публікувати» насправді означає «ставити тег». Отже, нам потрібен workflow, який знаходить останній тег `vX.Y.Z`, збільшує номер патча й пушить новий тег.

Для цього створіть файл `.github/workflows/release.yml` з таким вмістом:

```yaml
name: Release

on:
  push:
    branches: [main]

permissions:
  contents: write

concurrency:
  group: release
  queue: max

jobs:
  tag:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0

      - name: Tag the next patch version
        run: |
          git fetch origin --tags
          latest=$(git tag --list 'v*' --sort=-v:refname | sed -nE '/^v[0-9]+\.[0-9]+\.[0-9]+$/p' | sed -n '1p')
          latest=${latest:-v0.0.0}
          IFS=. read -r major minor patch <<< "${latest#v}"
          next="v$major.$minor.$((patch + 1))"
          git tag "$next"
          git push origin "$next"
          echo "Tagged $next"
```

Кілька деталей, які варто знати:

- `concurrency` ставить запуски випуску в чергу, щоб вони ставили теги по одному.
- `fetch-depth: 0` завантажує всю історію й теги; `git fetch origin --tags` оновлює теги перед вибором наступної версії.
- `--sort=-v:refname` ставить найновішу версію першою, тож `v1.10.0` іде перед `v1.9.0`. Фільтр лишає тільки теги `vX.Y.Z`, пропускаючи передрелізні версії й сторонні назви.
- `permissions: contents: write` дозволяє workflow пушити тег із вбудованим `GITHUB_TOKEN`. Особистий токен не потрібен.

Відтепер кожен пуш у `main` запускатиме патч-випуск, і Packagist підхопить його за кілька хвилин. Коли знадобиться мінорне чи мажорне підвищення версії, поставте тег вручну (`git tag v0.2.0 && git push --tags`), і workflow продовжить уже від нього.

### Передаємо інструмент агенту

Ми зробили інструмент зручним для агентів:

- Результати й оброблені помилки приходять у JSON зі стабільною структурою.
- Немає кольорового чи відформатованого тексту, який довелося б вичищати.
- Успішні запити погоди завершуються з кодом `0`, а оброблені помилки - з кодом `1`. Той, хто викликає команду, може перевірити успіх, не розбираючи вивід.
- Інструмент не інтерактивний: він ніколи не зупиняється, щоб щось запитати.

CPX теж підіграє. Він визначає, що запущений не в терміналі (AI-агентом, із перенаправленим вводом або з прапорцем `--no-interaction`), і переходить у неінтерактивний режим: жодного виводу прогресу, жодних запитань, тільки те, що виводить сам інструмент.

Якщо ж CPX не може виконати свою роботу - скажімо, у назві пакета друкарська помилка, - він теж відповідає в JSON:

```json
{
  "success": false,
  "errors": ["The package \"nicodevs/whether\" could not be found."],
  "summary": {}
}
```

Агент отримує JSON з обох рівнів - і від інструмента, і від того, хто його запускає.

Тепер можна навчити агента ним користуватися: описати інструмент в `AGENTS.md`, у навичці (skill) або просто в промпті. З боку агента не треба встановлювати нічого, крім PHP і CPX, а перший запуск сам встановить пакет.

```markdown
Whenever the user asks for the weather, run `cpx nicodevs/weather`
followed by the city, for example `cpx nicodevs/weather "Tokyo"`.
The command prints JSON with the city, temperature in Celsius and wind
speed in km/h. If the city isn't found, check its name and try again.
For other errors, report the problem instead of changing the city.
```

Українською ця інструкція звучить так: коли користувач питає про погоду, виконай `cpx nicodevs/weather` з назвою міста, наприклад `cpx nicodevs/weather "Tokyo"`; команда виводить JSON із містом, температурою в градусах Цельсія та швидкістю вітру в км/год; якщо місто не знайдено, перевір назву й спробуй ще раз; про інші помилки повідом, а не міняй місто.

Ось і вся інтеграція. Жодного API-клієнта, жодного SDK, жодного сервера - одна команда, яку агент може запустити будь-де, де встановлено PHP.

### Підсумок

Я обожнюю CPX! Він універсальний і дуже спрощує поширення пакетів. Інсталятори, CLI-інструменти, обгортки над API - можливості безмежні. Багато можливостей ми не встигли розглянути, тож неодмінно загляньте в [документацію CPX](https://cpx.dev/).

Звісно, інструмент погоди, який ми сьогодні збудували, сам по собі не надто корисний: будь-який здібний AI-агент звернувся б до API напряму. Але створити ідеальний інструмент погоди ніколи й не було метою цієї статті. Справжня мета - показати структуру, яку можна повторно використати, щоб будувати інструменти для власних проєктів.

Питання в тому, що збудуєте ви?

До зустрічі!
