# Bitrix24 AI Engine (AITUNNEL) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build a custom AI Engine endpoint that Bitrix24 can call as an alternative to its built-in Copilot agent, backed by AITUNNEL's `gpt-5.6-luna-pro` model.

**Architecture:** A small set of standalone PHP scripts (no framework, no Composer — matching the existing `AI_BOT_V2` codebase style). `completions.php` is the public endpoint Bitrix24 calls; it answers `202` immediately via `fastcgi_finish_request()`, then calls AITUNNEL and reports the result back to Bitrix24 via `callbackUrl`/`errorCallbackUrl`. `register.php`/`list.php`/`unregister.php` are one-off CLI scripts that manage the `ai.engine.*` registration on the Bitrix24 side.

**Tech Stack:** PHP (CLI + built-in `php -S` dev server for tests), curl, bash for integration test scripts. No external dependencies.

**Spec:** `docs/superpowers/specs/2026-08-20-bitrix24-ai-engine-design.md`

## Global Constraints

- Category: `text` only (no image/audio/call).
- Model: AITUNNEL `gpt-5.6-luna-pro`.
- No tool calls, no file handling — plain chat only.
- Do not validate `auth.access_token` from Bitrix24 requests.
- Use the `context` array from Bitrix24's request as conversation history.
- Deployment to the production server is done by the user, not by this plan — code only needs to be correct and locally testable.
- No existing test framework in this codebase — tests are plain PHP/bash scripts using PHP's built-in server (`php -S`) as local mocks, following the logging-and-manual-verification style already used in `AI_BOT_V2`.

---

### Task 1: Core config and AITUNNEL/Bitrix helper functions

**Files:**
- Create: `config.php`
- Create: `functions.php`
- Test: `tests/mock_aitunnel.php`
- Test: `tests/mock_callback.php`
- Test: `tests/test_functions.php`

**Interfaces:**
- Produces: `writeToLog($data, string $title = ''): void`, `resolveAIError(int $code): string`, `callAITunnel(array $messages): array` (returns `['result' => string]` on success or `['error' => string]` on failure), `notifyBitrix(string $url, array $payload): void`, `bx24query(array $queryData, string $method)` (returns decoded JSON array or `null`).
- Consumes: nothing (first task).

- [ ] **Step 1: Initialize git repository (project has none yet)**

```bash
cd /Users/genius/Desktop/AI/AI_AGENT_V3
git init
```

- [ ] **Step 2: Write the test fixtures and test script (fails first — nothing exists yet)**

Create `tests/mock_aitunnel.php`:

```php
<?php
// Мок-сервер AITUNNEL для тестов.
// Запуск: AITUNNEL_URL_OVERRIDE=... php -S localhost:8990 tests/mock_aitunnel.php
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$messages = $body['messages'] ?? [];
$lastContent = $messages ? end($messages)['content'] : '';

header('Content-Type: application/json');

if ($lastContent === 'trigger_error') {
    http_response_code(401);
    echo json_encode(['error' => ['code' => 401, 'message' => 'invalid api key']]);
    exit;
}

echo json_encode([
    'choices' => [
        ['message' => ['role' => 'assistant', 'content' => 'mock answer: ' . $lastContent]],
    ],
]);
```

Create `tests/mock_callback.php`:

```php
<?php
// Мок-приёмник callbackUrl/errorCallbackUrl.
// Запуск: php -S localhost:8991 tests/mock_callback.php
$body = file_get_contents('php://input');
file_put_contents(__DIR__ . '/callback_received.json', $body);
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
```

Create `tests/test_functions.php`:

```php
<?php
// Запуск (сервера должны быть подняты заранее — см. Step 4):
//   php tests/test_functions.php

require_once __DIR__ . '/../functions.php';

$failures = 0;

function check(bool $cond, string $label): void
{
    global $failures;
    if ($cond) {
        echo "OK   {$label}\n";
    } else {
        echo "FAIL {$label}\n";
        $failures++;
    }
}

// 1. Успешный вызов callAITunnel
$response = callAITunnel([
    ['role' => 'system', 'content' => 'sys'],
    ['role' => 'user', 'content' => 'hello'],
]);
check(isset($response['result']), 'callAITunnel: success returns result');
check(($response['result'] ?? '') === 'mock answer: hello', 'callAITunnel: result content matches mock');

// 2. Ошибка от AITUNNEL (401)
$errorResponse = callAITunnel([
    ['role' => 'user', 'content' => 'trigger_error'],
]);
check(isset($errorResponse['error']), 'callAITunnel: error path returns error key');
check(($errorResponse['error'] ?? '') === 'Ошибка авторизации AI-сервиса.', 'callAITunnel: 401 maps to readable message');

// 3. notifyBitrix отправляет тело на мок-приёмник
$receivedFile = __DIR__ . '/callback_received.json';
if (file_exists($receivedFile)) {
    unlink($receivedFile);
}
notifyBitrix('http://localhost:8991/mock_callback.php', ['result' => 'final answer']);
usleep(200000);
check(file_exists($receivedFile), 'notifyBitrix: mock receiver got a request');
check(
    (json_decode((string) file_get_contents($receivedFile), true)['result'] ?? '') === 'final answer',
    'notifyBitrix: payload content matches'
);

if ($failures > 0) {
    fwrite(STDERR, "\n{$failures} check(s) FAILED\n");
    exit(1);
}

echo "\nAll checks passed\n";
```

- [ ] **Step 3: Run the test to confirm it fails**

Run: `php tests/test_functions.php`
Expected: FAIL with a fatal error like `Failed opening required '.../functions.php'` (neither `config.php` nor `functions.php` exist yet).

- [ ] **Step 4: Implement `config.php`**

```php
<?php

define('AITUNNEL_API_KEY', 'sk-aitunnel-0H1gwP4EewVgEnwkMKFE1bjm8ZtzkZ6j');
define('AITUNNEL_URL', getenv('AITUNNEL_URL_OVERRIDE') ?: 'https://api.aitunnel.ru/v1/chat/completions');
define('AITUNNEL_MODEL', 'gpt-5.6-luna-pro');
define('AI_MAX_TOKENS', 3000);
define('SYSTEM_PROMPT', 'Ты — AI-ассистент, подключённый как AI Engine в Bitrix24. Отвечай чётко и по делу, на русском языке, если пользователь не пишет на другом.');

// ВНИМАНИЕ: вебхуку нужен scope ai_admin для ai.engine.* методов —
// проверь/дополни права у вебхука перед запуском register.php.
define('BX24_WEBHOOK', getenv('BX24_WEBHOOK_OVERRIDE') ?: 'https://aservice-24.bitrix24.ru/rest/8748/vqhsc2j7kirm5vyn/');
define('ENGINE_NAME', 'Acme Luna AI');
define('ENGINE_CODE', 'acme_luna_ai');
// Заполнить реальным публичным URL после выкладки completions.php на сервер.
define('COMPLETIONS_URL', 'https://REPLACE-ME/path/to/completions.php');

define('LOG_FILE', getcwd() . '/ai_engine.log');
```

- [ ] **Step 5: Implement `functions.php`**

```php
<?php

require_once __DIR__ . '/config.php';

function writeToLog($data, string $title = ''): void
{
    $log  = "\n------------------------\n";
    $log .= date('Y.m.d G:i:s') . "\n";
    $log .= (strlen($title) > 0 ? $title : 'DEBUG') . "\n";
    $log .= print_r($data, true);
    $log .= "\n------------------------\n";
    file_put_contents(LOG_FILE, $log, FILE_APPEND);
}

function resolveAIError(int $code): string
{
    switch ($code) {
        case 400: return 'Некорректный запрос. Попробуйте ещё раз.';
        case 401: return 'Ошибка авторизации AI-сервиса.';
        case 402: return 'Недостаточно средств на балансе AI-сервиса.';
        case 403: return 'Сообщение заблокировано модерацией.';
        case 408: return 'Превышено время ожидания ответа. Попробуйте ещё раз.';
        case 429: return 'Слишком много запросов. Попробуйте через несколько секунд.';
        case 502: return 'Модель временно недоступна. Попробуйте позже.';
        case 504: return 'Превышено время ожидания ответа от модели. Попробуйте ещё раз.';
        default:  return "Ошибка AI-сервиса (код {$code}). Попробуйте позже.";
    }
}

/**
 * @return array{result?: string, error?: string}
 */
function callAITunnel(array $messages): array
{
    $payload = [
        'model'      => AITUNNEL_MODEL,
        'messages'   => $messages,
        'max_tokens' => AI_MAX_TOKENS,
    ];

    $curl = curl_init(AITUNNEL_URL);
    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . AITUNNEL_API_KEY,
            'Content-Type: application/json',
        ],
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_TIMEOUT        => 300,
        CURLOPT_CONNECTTIMEOUT => 15,
    ]);

    $raw       = curl_exec($curl);
    $httpCode  = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    $curlError = curl_error($curl);
    $curlErrNo = curl_errno($curl);
    curl_close($curl);

    if ($raw === false) {
        writeToLog(['curl_errno' => $curlErrNo, 'curl_error' => $curlError], 'AI_CURL_ERROR');
        if ($curlErrNo === 28) {
            return ['error' => 'Превышено время ожидания ответа от модели. Попробуйте ещё раз.'];
        }
        return ['error' => "Ошибка соединения с AI-сервисом ({$curlError}). Попробуйте позже."];
    }

    if ($httpCode !== 200) {
        $decoded = json_decode($raw, true);
        $code    = $decoded['error']['code'] ?? $httpCode;
        writeToLog(['httpCode' => $httpCode, 'code' => $code, 'response' => $raw], 'AI_ERROR');
        return ['error' => resolveAIError((int) $code)];
    }

    $decoded = json_decode($raw, true);
    $text    = $decoded['choices'][0]['message']['content'] ?? null;

    if ($text === null) {
        writeToLog(['response' => $raw], 'AI_EMPTY_RESPONSE');
        return ['error' => 'Пустой ответ от модели.'];
    }

    return ['result' => $text];
}

function notifyBitrix(string $url, array $payload): void
{
    $curl = curl_init($url);
    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_TIMEOUT        => 15,
    ]);

    $raw      = curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);

    writeToLog(['url' => $url, 'payload' => $payload, 'httpCode' => $httpCode, 'response' => $raw], 'NOTIFY_BITRIX');
}

function bx24query(array $queryData, string $method)
{
    $curl = curl_init();
    curl_setopt_array($curl, [
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_URL            => BX24_WEBHOOK . $method,
        CURLOPT_POSTFIELDS     => json_encode($queryData),
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
    ]);
    $raw = curl_exec($curl);
    curl_close($curl);

    return $raw === false ? null : json_decode($raw, true);
}
```

- [ ] **Step 6: Run the test to confirm it passes**

Run (three commands, mock servers backgrounded):

```bash
cd /Users/genius/Desktop/AI/AI_AGENT_V3
AITUNNEL_URL_OVERRIDE=http://localhost:8990/mock_aitunnel.php php -S localhost:8990 tests/mock_aitunnel.php &
php -S localhost:8991 tests/mock_callback.php &
sleep 0.3
AITUNNEL_URL_OVERRIDE=http://localhost:8990/mock_aitunnel.php php tests/test_functions.php
kill %1 %2
```

Expected: `All checks passed` with no `FAIL` lines.

- [ ] **Step 7: Commit**

```bash
cd /Users/genius/Desktop/AI/AI_AGENT_V3
git add config.php functions.php tests/mock_aitunnel.php tests/mock_callback.php tests/test_functions.php
git commit -m "feat: add AITUNNEL/Bitrix helper functions with tests"
```

---

### Task 2: `completions.php` — the AI Engine endpoint

**Files:**
- Create: `completions.php`
- Test: `tests/test_completions.sh`

**Interfaces:**
- Consumes: `callAITunnel(array $messages): array`, `notifyBitrix(string $url, array $payload): void`, `writeToLog($data, string $title = ''): void`, constant `SYSTEM_PROMPT` (all from Task 1's `functions.php`/`config.php`).
- Produces: HTTP endpoint at `completions.php` — POST body `{prompt, context, payload_role?, callbackUrl, errorCallbackUrl}` → `202 {"result":"OK"}` immediately, then async POST to `callbackUrl` with `{"result": "<text>"}` or to `errorCallbackUrl` with `{"error": "SERVICE_ERROR", "error_description": "<text>"}`.

- [ ] **Step 1: Write the integration test (fails first — `completions.php` doesn't exist)**

Create `tests/test_completions.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."

AITUNNEL_URL_OVERRIDE="http://localhost:8990/mock_aitunnel.php" php -S localhost:8990 tests/mock_aitunnel.php >/tmp/mock_aitunnel.log 2>&1 &
AITUNNEL_PID=$!
php -S localhost:8991 tests/mock_callback.php >/tmp/mock_callback.log 2>&1 &
CALLBACK_PID=$!
AITUNNEL_URL_OVERRIDE="http://localhost:8990/mock_aitunnel.php" php -S localhost:8989 >/tmp/app_server.log 2>&1 &
APP_PID=$!

trap 'kill $AITUNNEL_PID $CALLBACK_PID $APP_PID 2>/dev/null || true' EXIT
sleep 0.4

FAIL=0

echo "--- success path ---"
rm -f tests/callback_received.json
HTTP_CODE=$(curl -s -o /tmp/completions_response.json -w '%{http_code}' \
  -X POST http://localhost:8989/completions.php \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"hello","context":[{"role":"user","content":"previous msg"}],"callbackUrl":"http://localhost:8991/mock_callback.php","errorCallbackUrl":"http://localhost:8991/mock_callback.php"}')

if [ "$HTTP_CODE" != "202" ]; then
  echo "FAIL: expected HTTP 202, got $HTTP_CODE"
  FAIL=1
else
  echo "OK: HTTP 202 received"
fi

grep -q '"result":"OK"' /tmp/completions_response.json && echo "OK: immediate body correct" || { echo "FAIL: immediate body wrong"; FAIL=1; }

sleep 0.5
if grep -q '"result":"mock answer: hello"' tests/callback_received.json 2>/dev/null; then
  echo "OK: callback received correct result"
else
  echo "FAIL: callback did not receive expected result"
  cat tests/callback_received.json 2>/dev/null || echo "(no file)"
  FAIL=1
fi

echo "--- error path ---"
rm -f tests/callback_received.json
curl -s -o /dev/null -X POST http://localhost:8989/completions.php \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"trigger_error","context":[],"callbackUrl":"http://localhost:8991/mock_callback.php","errorCallbackUrl":"http://localhost:8991/mock_callback.php"}'

sleep 0.5
if grep -q '"error_description":"\\u041e\\u0448\\u0438\\u0431\\u043a\\u0430 \\u0430\\u0432\\u0442\\u043e\\u0440\\u0438\\u0437\\u0430\\u0446\\u0438\\u0438 AI-\\u0441\\u0435\\u0440\\u0432\\u0438\\u0441\\u0430."' tests/callback_received.json 2>/dev/null || grep -q 'Ошибка авторизации AI-сервиса' tests/callback_received.json 2>/dev/null; then
  echo "OK: error callback received correct message"
else
  echo "FAIL: error callback missing/incorrect"
  cat tests/callback_received.json 2>/dev/null || echo "(no file)"
  FAIL=1
fi

if [ "$FAIL" -ne 0 ]; then
  echo "SOME CHECKS FAILED"
  exit 1
fi
echo "All completions.php checks passed"
```

Make it executable:

```bash
chmod +x tests/test_completions.sh
```

- [ ] **Step 2: Run the test to confirm it fails**

Run: `./tests/test_completions.sh`
Expected: FAIL — `completions.php` doesn't exist yet, so the built-in server returns `404` and `HTTP_CODE` check fails.

- [ ] **Step 3: Implement `completions.php`**

```php
<?php

require_once __DIR__ . '/functions.php';

$raw  = file_get_contents('php://input');
$body = json_decode($raw, true) ?? [];

writeToLog($body, 'COMPLETIONS_REQUEST');

$prompt           = $body['prompt'] ?? '';
$context          = $body['context'] ?? [];
$payloadRole      = $body['payload_role'] ?? null;
$callbackUrl      = $body['callbackUrl'] ?? null;
$errorCallbackUrl = $body['errorCallbackUrl'] ?? null;

if (!$callbackUrl || !$errorCallbackUrl) {
    writeToLog($body, 'COMPLETIONS_MISSING_CALLBACK');
    http_response_code(400);
    header('Content-Type: application/json');
    echo json_encode(['error' => 'callbackUrl/errorCallbackUrl required']);
    exit;
}

http_response_code(202);
header('Content-Type: application/json');
echo json_encode(['result' => 'OK']);

if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();
} else {
    // php -S (встроенный сервер разработки) не поддерживает fastcgi_finish_request —
    // используется только локально для тестов, на проде будет php-fpm.
    flush();
}

$messages = [
    ['role' => 'system', 'content' => $payloadRole ?? SYSTEM_PROMPT],
];

foreach ($context as $item) {
    if (isset($item['role'], $item['content'])) {
        $messages[] = ['role' => $item['role'], 'content' => $item['content']];
    }
}

$messages[] = ['role' => 'user', 'content' => $prompt];

$response = callAITunnel($messages);

if (isset($response['error'])) {
    notifyBitrix($errorCallbackUrl, [
        'error'             => 'SERVICE_ERROR',
        'error_description' => $response['error'],
    ]);
    exit;
}

notifyBitrix($callbackUrl, ['result' => $response['result']]);
```

- [ ] **Step 4: Run the test to confirm it passes**

Run: `./tests/test_completions.sh`
Expected: `All completions.php checks passed`, no `FAIL` lines.

- [ ] **Step 5: Commit**

```bash
cd /Users/genius/Desktop/AI/AI_AGENT_V3
git add completions.php tests/test_completions.sh
git commit -m "feat: add async completions.php AI Engine endpoint"
```

---

### Task 3: `register.php` / `list.php` / `unregister.php` — engine management scripts

**Files:**
- Create: `register.php`
- Create: `list.php`
- Create: `unregister.php`
- Test: `tests/mock_bx24.php`
- Test: `tests/test_engine_scripts.sh`

**Interfaces:**
- Consumes: `bx24query(array $queryData, string $method)`, `writeToLog($data, string $title = ''): void` (from Task 1), constants `ENGINE_NAME`, `ENGINE_CODE`, `COMPLETIONS_URL`, `AI_MAX_TOKENS` (from Task 1's `config.php`).
- Produces: three CLI-runnable scripts with human-readable stdout output (`id = N`, `id=N code=... name=... category=... completions_url=...` per line, `id=N удалён.`).

- [ ] **Step 1: Write the test fixture and test script (fails first — scripts don't exist)**

Create `tests/mock_bx24.php`:

```php
<?php
// Мок REST-сервера Bitrix24 для тестов ai.engine.*.
// Запуск: php -S localhost:8992 tests/mock_bx24.php
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$body = json_decode(file_get_contents('php://input'), true) ?? [];

header('Content-Type: application/json');

if ($path === '/ai.engine.register') {
    echo json_encode(['result' => 99]);
    exit;
}

if ($path === '/ai.engine.list') {
    echo json_encode(['result' => [[
        'id'              => 99,
        'app_code'        => 'test_app',
        'name'            => $body['name'] ?? 'n/a',
        'code'            => 'acme_luna_ai',
        'category'        => 'text',
        'completions_url' => 'http://example.test/completions.php',
    ]]]);
    exit;
}

if ($path === '/ai.engine.unregister') {
    echo json_encode(['result' => true]);
    exit;
}

http_response_code(404);
echo json_encode(['error' => 'unknown method']);
```

Create `tests/test_engine_scripts.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."

php -S localhost:8992 tests/mock_bx24.php >/tmp/mock_bx24.log 2>&1 &
MOCK_PID=$!
trap 'kill $MOCK_PID 2>/dev/null || true' EXIT
sleep 0.3

FAIL=0

echo "--- register.php ---"
OUT=$(BX24_WEBHOOK_OVERRIDE="http://localhost:8992/" php register.php)
echo "$OUT"
echo "$OUT" | grep -q "id = 99" || { echo "FAIL: register.php did not report id 99"; FAIL=1; }

echo "--- list.php ---"
OUT=$(BX24_WEBHOOK_OVERRIDE="http://localhost:8992/" php list.php)
echo "$OUT"
echo "$OUT" | grep -q "code=acme_luna_ai" || { echo "FAIL: list.php missing expected entry"; FAIL=1; }

echo "--- unregister.php ---"
OUT=$(BX24_WEBHOOK_OVERRIDE="http://localhost:8992/" php unregister.php 99)
echo "$OUT"
echo "$OUT" | grep -q "id=99" || { echo "FAIL: unregister.php did not confirm deletion"; FAIL=1; }

if [ "$FAIL" -ne 0 ]; then
  echo "SOME CHECKS FAILED"
  exit 1
fi
echo "All engine-script checks passed"
```

Make it executable:

```bash
chmod +x tests/test_engine_scripts.sh
```

- [ ] **Step 2: Run the test to confirm it fails**

Run: `./tests/test_engine_scripts.sh`
Expected: FAIL — `php: Unable to open file register.php` (or equivalent) since none of the three scripts exist yet.

- [ ] **Step 3: Implement `register.php`**

```php
<?php

require_once __DIR__ . '/functions.php';

$result = bx24query([
    'name'            => ENGINE_NAME,
    'code'            => ENGINE_CODE,
    'category'        => 'text',
    'completions_url' => COMPLETIONS_URL,
    'settings'        => [
        'code_alias'          => 'Luna Pro',
        'model_context_type'  => 'token',
        'model_context_limit' => AI_MAX_TOKENS,
    ],
], 'ai.engine.register');

writeToLog($result, 'ENGINE_REGISTER');

if (isset($result['error'])) {
    fwrite(STDERR, 'Ошибка регистрации: ' . ($result['error_description'] ?? $result['error']) . "\n");
    exit(1);
}

echo 'Зарегистрировано, id = ' . ($result['result'] ?? '?') . "\n";
```

- [ ] **Step 4: Implement `list.php`**

```php
<?php

require_once __DIR__ . '/functions.php';

$result = bx24query(['filter' => [], 'limit' => 50], 'ai.engine.list');

writeToLog($result, 'ENGINE_LIST');

foreach ($result['result'] ?? [] as $engine) {
    printf(
        "id=%s code=%s name=%s category=%s completions_url=%s\n",
        $engine['id'] ?? '?',
        $engine['code'] ?? '?',
        $engine['name'] ?? '?',
        $engine['category'] ?? '?',
        $engine['completions_url'] ?? '?'
    );
}
```

- [ ] **Step 5: Implement `unregister.php`**

```php
<?php

require_once __DIR__ . '/functions.php';

$id = $argv[1] ?? null;

if (!$id) {
    fwrite(STDERR, "Использование: php unregister.php <id>\n");
    exit(1);
}

$result = bx24query(['id' => (int) $id], 'ai.engine.unregister');

writeToLog($result, 'ENGINE_UNREGISTER');

if (isset($result['error'])) {
    fwrite(STDERR, 'Ошибка удаления: ' . ($result['error_description'] ?? $result['error']) . "\n");
    exit(1);
}

echo "Сервис id={$id} удалён.\n";
```

- [ ] **Step 6: Run the test to confirm it passes**

Run: `./tests/test_engine_scripts.sh`
Expected: `All engine-script checks passed`, no `FAIL` lines.

- [ ] **Step 7: Commit**

```bash
cd /Users/genius/Desktop/AI/AI_AGENT_V3
git add register.php list.php unregister.php tests/mock_bx24.php tests/test_engine_scripts.sh
git commit -m "feat: add ai.engine register/list/unregister CLI scripts"
```

---

## After implementation (manual, on the real server — not part of this plan)

1. Upload all files (`config.php`, `functions.php`, `completions.php`, `register.php`, `list.php`, `unregister.php`) to the server, in the same place as `AI_BOT_V2`.
2. Confirm the webhook in `BX24_WEBHOOK` has the `ai_admin` scope (the existing `AI_BOT_V2` webhook may not — check/extend it in Bitrix24's webhook settings).
3. Edit `config.php` on the server: set `COMPLETIONS_URL` to the real public URL of the uploaded `completions.php`.
4. Run `php register.php` on the server once, note the returned `id`.
5. Run `php list.php` to confirm the engine shows up with the right `completions_url`.
6. Test end-to-end from Bitrix24's UI (select the new engine as an AI provider) and check `ai_engine.log` for the request/response trail.
