# Работа с AI-агентами

PlugOnce предоставляет локальный CLI любому агенту, который умеет выполнять команды на Mac от имени того же пользователя. Провайдер, модель, MCP, Raycast и отдельный скил для этого не нужны. Проверяется общий CLI, а не совместимость каждого продукта или модели.

Начиная с **0.6.6 (44)**: справка по каждому методу, офлайн `capabilities`, типизированный ответ `--api-version 2`, чтение папок через `--storage-id` + `--path`. CLI использует существующий Agent Bridge, очередь и журнал операций.

## Подготовка пользователя

1. Установите PlugOnce в `/Applications`, запустите приложение.
2. Подключите Switch по USB и откройте **DBI → Run MTP responder**. Подключите найденное устройство в PlugOnce.
3. В **Настройки → Agent Access** включите доступ. Для чтения разрешите `device.status`, `files.list`, `operation.get`. Поиск требует `files.search`; избранное — `favorites.list`.
4. Разрешите только нужные пути назначения. `/` разрешает всё выбранное хранилище; `/switch` ограничивает работу этой папкой. Для передачи отдельно разрешите локальный каталог источников и действия `transfer.plan`, `transfer.commit`. Не поручайте агенту самостоятельно расширять доступ.
5. Оставьте приложение открытым. По умолчанию запись подтверждает пользователь в PlugOnce; автономный доступ включается отдельно, как описано ниже. Агенту управление мышью не требуется.

Сам CLI не включает Agent Access и не подключает устройство. При отсутствии доступа он возвращает инструкцию по настройке. Настройка и согласие пользователя остаются его действиями.

## Обнаружение и первая команда

Примеры команд — **fish**. Следующие ответы взяты из автоматической проверки настоящего CLI через локальный MTP-симулятор, а не физический Switch. Для читаемости часть полей опущена; UUID и пути теста действительны только для того запуска. Для своего устройства всегда используйте свежие значения из своего ответа.

```fish
set p /Applications/PlugOnce.app/Contents/MacOS/PlugOnce
$p --agent capabilities
```

Форма сокращённого ответа офлайн-обнаружения; путь показан для установки в `/Applications` (в проверке он указывал на Resources собранного приложения):

```json
{"cliResponseVersions":[1,2],"defaultResponseVersion":1,"recommendedResponseVersion":2,"socketProtocolVersion":1,"documentation":"/Applications/PlugOnce.app/Contents/Resources/AI_AGENTS.md"}
```

Поле `documentation` даёт путь к руководству, встроенному в установленное приложение; агент может прочитать его локальной командой, без репозитория и интернета. `websiteDocumentationURL` — копия для сайта. Полный ответ включает методы, обязательные/необязательные параметры, эффекты (`read`, `plan`, `navigate`, `write`, `cancel`), примеры, подтверждение, срок жизни ID и exit codes. Обнаружение доступно даже при остановленном приложении; оно описывает поддержку, а не предоставленные разрешения.

```fish
$p --agent files.list --help
```

Сокращённый текст справки:

```text
files.list [read]
Required: --storage-id ID --path PATH (or --favorite-id UUID)
Optional: --device-id ID; --connection-id ID; --limit 1..200; --offset N (use pagination.nextOffset); --id UUID; --api-version 1|2; --socket ABSOLUTE_PATH
```

Следующий шаг — проверить устройство:

```fish
$p --agent device.status --api-version 2
```

```json
{
  "ok": true,
  "requestId": "941A4882-106C-46E0-A8CF-950B2BF62731",
  "result": {
    "device": {
      "connected": true,
      "connectionId": "1",
      "deviceId": "057E:3000:DBI:PLUGONCE-SIM-0001"
    },
    "storages": [
      {
        "capacityBytes": 1073741824,
        "freeBytes": 1073741824,
        "name": "SD Card",
        "role": "sdCard",
        "storageId": 1,
        "writable": true
      },
      {
        "capacityBytes": 1073741824,
        "freeBytes": 1073741824,
        "name": "Installed games",
        "role": "installedGames",
        "storageId": 2,
        "writable": false
      },
      {
        "capacityBytes": 1073741824,
        "freeBytes": 1073741824,
        "name": "SD Card install",
        "role": "installer",
        "storageId": 3,
        "writable": true
      },
      {
        "capacityBytes": 1073741824,
        "freeBytes": 1073741824,
        "name": "Saves",
        "role": "saves",
        "storageId": 4,
        "writable": false
      },
      {
        "capacityBytes": 1073741824,
        "freeBytes": 1073741824,
        "name": "Album",
        "role": "album",
        "storageId": 5,
        "writable": false
      }
    ],
    "nextAction": "Choose a storageId from storages; files.list --storage-id ID --path /."
  },
  "v": 2
}
```

Выберите нужное хранилище **из `storages`** по названию и роли. В проверке SD Card получил ID `1`; этот ID не является константой. `writable` — право записи устройства, а не разрешение Agent Access. `capacityBytes` и `freeBytes` отсутствуют при неизвестном значении; агент не должен считать его нулём.

При отключённом устройстве та же команда возвращает успешный снимок с `device.connected: false`, пустыми хранилищами и указанием подключить DBI. Чтение файлов тогда возвращает `NO_DEVICE`.

## Корень, вложенная и пустая папка

В проверенном примере:

```fish
set storage_id 1 # Только ID, полученный из вашего device.status
$p --agent files.list --storage-id $storage_id --path / --limit 1 --api-version 2
```

```json
{
  "ok": true,
  "requestId": "7C991D49-D40F-4824-B3F8-86A869F9BDB9",
  "result": {
    "files": [
      {
        "name": "AI Demo Русский",
        "path": "/AI Demo Русский",
        "sizeBytes": null,
        "type": "directory"
      }
    ],
    "pagination": {
      "limit": 1,
      "nextAction": "Repeat files.list with the same storage/path and --offset 1.",
      "nextOffset": 1,
      "offset": 0,
      "omittedInvalidNames": 0,
      "totalCount": 7,
      "truncated": true
    }
  },
  "v": 2
}
```

Путь задаётся относительно выбранного хранилища: `/` — его корень, `/switch` — папка switch. Не используйте путь Mac, `..`, двойной слеш или завершающий слеш. `objectId` и дублирование `destination`/`parentPath` для чтения не нужны. У файлов есть `name`, `type`, полный путь в хранилище и `sizeBytes`; у каталогов и неизвестного размера `sizeBytes: null`. Пустой файл имеет число `0`.

Если `pagination.truncated: true`, повторите ту же команду с `--offset` из `nextOffset`. `totalCount` описывает текущий снимок, не постоянную последовательность: при изменениях папки между запросами повторите чтение с нуля. Невалидные имена явно учитываются в `omittedInvalidNames`.

```fish
$p --agent files.list --storage-id $storage_id --path / --offset 1 --api-version 2
```

Ответ содержит следующую страницу `files` и новое `pagination`; завершайте при `truncated: false`. Не переносите offset на другую папку.

```fish
$p --agent files.list --storage-id $storage_id --path '/AI Demo Русский' --api-version 2
```

```json
{
  "ok": true,
  "requestId": "23DAC198-AF59-48A9-9473-DD9B1972E261",
  "result": {
    "files": [
      {
        "name": "empty",
        "path": "/AI Demo Русский/empty",
        "sizeBytes": null,
        "type": "directory"
      },
      {
        "name": "файл с пробелом.txt",
        "path": "/AI Demo Русский/файл с пробелом.txt",
        "sizeBytes": 12,
        "type": "file"
      }
    ],
    "pagination": {
      "limit": 100,
      "offset": 0,
      "omittedInvalidNames": 0,
      "totalCount": 2,
      "truncated": false
    }
  },
  "v": 2
}
```

Выберите путь каталога из ответа и вызовите `files.list` с ним. Названия с пробелами и Unicode передавайте одним аргументом. Это переход для агента; открывать папку в интерфейсе не нужно.

```fish
$p --agent files.list --storage-id $storage_id --path '/AI Demo Русский/empty' --api-version 2
```

```json
{
  "ok": true,
  "requestId": "A84D63E3-B4D0-4799-9F6B-BCD5D525C149",
  "result": {
    "files": [],
    "pagination": {
      "limit": 100,
      "offset": 0,
      "omittedInvalidNames": 0,
      "totalCount": 0,
      "truncated": false
    }
  },
  "v": 2
}
```

Пустой массив — успешное чтение пустой папки. Поиск `files.search --storage-id ID --path PATH --query TEXT` ограничен 100 папками, глубиной 32, очередью 1000 и бюджетом 8 секунд между запросами к устройству. Для поиска `truncated: true` требует сузить путь или запрос; offset у поиска нет. Длительность отдельного MTP-запроса может превысить бюджет.

## Разрешение автономной работы

Начиная с **0.6.7 (45)** пользователь может включить в **Настройки → Agent Access** два отдельных права:

- **Передавать без подтверждения каждый раз** — новые файлы и папки.
- **Заменять существующие файлы без подтверждения** — дополнительно разрешает замену одноимённых файлов внутри выбранных папок.

Оба права по умолчанию выключены. Право замены действует вместе с правом автономной передачи. Список разрешённых методов, Mac source roots и device destination roots продолжает ограничивать каждую операцию. Эти настройки нельзя изменить через Agent API.

`device.status` возвращает `result.access`: `allowedActions`, `sourceRoots`, `destinationRoots`, boolean `unattendedTransfers` и `unattendedReplacements`. Сверяйте область доступа перед планированием. Права относятся ко всем локальным процессам этого пользователя, которым доступен bridge; отдельная идентификация агентов не добавлена.

Перед каждой записью по-прежнему нужен `transfer.plan`. Его `requiresConfirmation` и `nextAction` учитывают текущие разрешения. Если `requiresConfirmation: false`, агент сохраняет UUID и вызывает `transfer.commit --id UUID --plan-id PLAN_ID --confirm`; отдельного диалога PlugOnce для этой передачи не будет. `--confirm` выражает намерение выполнить план и остаётся обязательным. Legacy `transfer.enqueue` сохраняет отдельное подтверждение.

Если право замены выключено, план с конфликтами потребует пользователя, даже когда новые файлы разрешены автоматически. Изменение прав после планирования проверяется заново при commit, перед следующими файлами и непосредственно перед атомарной заменой. Отзыв права останавливает следующие шаги; уже начатая атомарная замена завершается или откатывается для сохранения целостности. Запись в NAND сохраняет дополнительное подтверждение. Самостоятельное удаление отдельных файлов этот API не предоставляет.

Следующий пример передачи показывает режим по умолчанию, с подтверждением пользователя.

## План, подтверждение, результат

Для теста пользователь должен разрешить конкретный безопасный источник и назначение. Не отправляйте тестовые файлы на настоящий Switch без его согласия.

```fish
set plan_request (uuidgen)
$p --agent transfer.plan --id $plan_request --storage-id $storage_id \
    --source '/private/tmp/plugonce-agent-demo/video demo.txt' --kind file \
    --destination '/AI Demo Русский/video demo.txt' --api-version 2
```

Команда показывает форму вызова; локальный источник и папка назначения должны существовать. В автоматической проверке источник был во временном каталоге. Сокращённый **фактический** ответ:

```json
{
  "ok": true,
  "requestId": "30404A23-B231-4FAF-A7C7-F0B5B8F81423",
  "result": {
    "state": "planned",
    "planId": "30404A23-B231-4FAF-A7C7-F0B5B8F81423",
    "plan": {
      "conflicts": [],
      "connectionId": "1",
      "destination": "/AI Demo Русский/video demo.txt",
      "deviceId": "057E:3000:DBI:PLUGONCE-SIM-0001",
      "fileCount": 1,
      "folderCount": 0,
      "kind": "file",
      "nextAction": "Show this plan to the user; transfer.commit --plan-id PLAN_ID --confirm requests approval in PlugOnce. Poll operation.get using the commit request UUID while approval is pending.",
      "requiresConfirmation": true,
      "sizeBytes": 12,
      "source": "/tmp/ac-D5AFFB69-9AD0-4FA0-9A2F-3CEB47D16157/video demo.txt",
      "storageId": 1
    },
    "nextAction": "Show this plan to the user; transfer.commit --plan-id PLAN_ID --confirm requests approval in PlugOnce. Poll operation.get using the commit request UUID while approval is pending."
  },
  "v": 2
}
```

Покажите пользователю источник, устройство, хранилище, назначение, объём и `conflicts`. Конфликты — пути внутри передаваемого дерева; пустая строка означает существующий корневой элемент передачи. `requiresConfirmation: true` не разрешает запись. Типы источников: `file`, `folder`, `layeredfs`. Для file/folder назначение включает неизменённое имя источника. Для проверенного LayeredFS (`atmosphere/contents/<title ID>`) назначение `/` допустимо только на SD-карте DBI.

Сохраните возвращённый `planId` и отдельный UUID запроса commit:

```fish
set plan_id UUID_ИЗ_ВАШЕГО_ОТВЕТА
set commit_request (uuidgen)
$p --agent transfer.commit --id $commit_request --plan-id $plan_id --confirm --api-version 2
```

Сокращённый ответ commit после подтверждения в автоматической проверке:

```json
{
  "ok": true,
  "requestId": "EEC4832E-B1CB-48C9-8F01-0E48C48B3F48",
  "result": {
    "nextAction": "Poll operation.get --operation-id EEC4832E-B1CB-48C9-8F01-0E48C48B3F48 until terminal is true; do not submit another write.",
    "operation": {
      "awaitingUserConfirmation": false,
      "operationId": "EEC4832E-B1CB-48C9-8F01-0E48C48B3F48",
      "state": "queued",
      "succeeded": false,
      "terminal": false
    },
    "planId": "30404A23-B231-4FAF-A7C7-F0B5B8F81423",
    "state": "queued"
  },
  "v": 2
}
```

Этот ответ означает принятие в очередь. Окончательный результат получают отдельным опросом.

В показанном режиме по умолчанию `--confirm` запрашивает согласие пользователя в приложении. До подтверждения данные не записываются. При явно включённом автономном доступе согласие заменяет ограниченное разрешение Agent Access; смотрите `requiresConfirmation` и `access`. CLI ждёт не более 15 секунд; при более долгом согласовании вернётся exit 2 / `BUSY`, а операция может остаться принятой. **Не создавайте другой UUID записи**: проверяйте исходный commit UUID.

```fish
$p --agent operation.get --operation-id $commit_request --api-version 2
```

Снимок проверки до подтверждения:

```json
{
  "ok": true,
  "requestId": "E70C48CC-15CE-4FC7-93D2-5FB329219A54",
  "result": {
    "state": "pending",
    "operation": {
      "awaitingUserConfirmation": true,
      "operationId": "EEC4832E-B1CB-48C9-8F01-0E48C48B3F48",
      "state": "pending",
      "succeeded": false,
      "terminal": false
    },
    "nextAction": "Ask the user to approve the shown plan in PlugOnce. Poll operation.get --operation-id EEC4832E-B1CB-48C9-8F01-0E48C48B3F48; do not submit another write."
  },
  "v": 2
}
```

Попросите пользователя подтвердить план в PlugOnce и продолжайте опрашивать этот ID. Если он отказался, покажите отмену. Окончательный снимок после подтверждения на симуляторе:

```json
{
  "ok": true,
  "requestId": "C56FEE29-76ED-4E10-AA56-704966384A5B",
  "result": {
    "state": "completed",
    "operation": {
      "awaitingUserConfirmation": false,
      "completedBytes": 12,
      "operationId": "EEC4832E-B1CB-48C9-8F01-0E48C48B3F48",
      "state": "completed",
      "succeeded": true,
      "terminal": true,
      "totalBytes": 12,
      "verification": "remoteListingVerified"
    },
    "nextAction": "Finished. Inspect succeeded, message and verification. completed describes transfer, not console installation."
  },
  "v": 2
}
```

Запрос отмены активной передачи может сначала вернуть `cancelling` и `terminal: false`: дождитесь фактического завершения по исходному operationId. Остановите опрос при `operation.terminal: true`; оцените `succeeded`, `message` и `verification`. `remoteListingVerified` подтверждает удалённую запись метаданных, а не SHA-256. В тесте дополнительно прочитаны байты файла симулятора и сравнены с источником. Передача в DBI install storage может закончиться `sentToDBIUnverified`; это не доказательство установки на консоли. Никогда не объявляйте «установлено», если этого нет в проверке устройства.

## Контракт, идентификаторы и совместимость

- stdout команд содержит один JSON-ответ, без журналов; `--help` возвращает текст. Диагностика остаётся в stderr.
- `--api-version 1` (по умолчанию) сохраняет старые поля/типы. Версия 2 — явное представление CLI с числовыми байтами и boolean, верхним `requestId`, отдельными `deviceId`, `storageId`, `planId`, `operationId`. Unix socket остаётся `agent-v1.sock`, `v:1`; второй транспорт не создан. Прямые socket-клиенты, включая Raycast, сохраняют старый контракт.
- JSON-числа объёма передаются как UInt64. Клиент JavaScript должен учитывать точность чисел больше `2^53-1`; не округляйте при проверке объёма. Для точного 64-битного обмена старый v1 сохраняет десятичные строки.
- `requestId` коррелирует вызов. UUID планирования также служит `planId`; UUID commit — `operationId`. Семантические роли различаются, даже если реализация повторно использует UUID.
- `deviceId` — идентичность устройства; `connectionId` — поколение соединения. После переподключения обновляйте статус/хранилища и создавайте новый план. Передавайте `--device-id` и `--connection-id` из статуса для привязки чтения/плана к снимку.
- `storageId` берётся из текущего подключения, `planId` действует только в текущей сессии приложения/устройства при неизменных источнике и назначении. `operationId` хранится в долговечном журнале и доступен после перезапуска.
- Для plan/commit/cancel сохраняйте UUID **до** запуска. Повтор того же UUID с теми же параметрами возвращает прежнюю операцию; другое содержимое запроса с тем же UUID отклоняется. Один план нельзя незаметно исполнить второй раз. Чтение по одному UUID всегда свежее.
- Если ответ планирования потерян, `operation.get` по UUID планирования возвращает сохранённый план, когда он готов. После рестарта незавершённая запись становится `interrupted`, автоматически не возобновляется. Проверьте устройство и результат до нового плана.
- Exit codes: **0** — успех/справка; **1** — отклонение API; **2** — ошибка CLI, недоступный bridge или неопределённый транспортный результат. При exit 2 после записи сохраняйте исходный UUID и проверяйте журнал.

## Ошибки и восстановление

| Код | Значение | Следующий шаг |
| --- | --- | --- |
| `UNSUPPORTED` | Метод отсутствует | `capabilities`, `METHOD --help` |
| `UNAUTHORIZED` | Нет разрешения | `error.details.requiredPermission` и `method` указывают настройку Agent Access; пользователь включает только нужный доступ |
| `BRIDGE_UNAVAILABLE` | Приложение/bridge не запущено | Запустить PlugOnce, проверить Agent Access |
| `NO_DEVICE` | Нет устройства или сменилось подключение | DBI/USB, новый status, новый план |
| `BUSY` | Временная занятость или таймаут | Повторить чтение позже; для записи опросить исходный UUID |
| `BAD_REQUEST` | Неверный путь/ID/параметры | Справка метода и свежие ID из статуса |
| `SOURCE_CHANGED` | Источник или утверждённая область изменились | Новый план и новое согласие |
| `INVALID_PACKAGE` | Неверная структура пакета | Проверить пакет локально |
| `CANCELLED` | Отказ/отмена | Проверить итог и назначение |
| `INTERNAL` | Ошибка выполнения/журнала | Не повторять запись автоматически; опросить журнал, сохранить ошибку |

Пример отказа в разрешении из проверки:

```fish
$p --agent files.list --storage-id $storage_id --path / --api-version 2
```

```json
{
  "error": {
    "code": "UNAUTHORIZED",
    "details": {
      "method": "files.list",
      "requiredPermission": "action",
      "settings": "Agent Access"
    },
    "message": "This request is not permitted by Agent Access.",
    "nextAction": "In PlugOnce Settings > Agent Access, enable access, the named action and necessary source/destination roots. Ask the user; do not change permissions yourself."
  },
  "ok": false,
  "requestId": "45404EBA-BC2D-4E87-932C-EAAE6D6DAD4B",
  "v": 2
}
```

Ошибки содержат стабильный `code`, описание, структурированные детали по необходимости и `nextAction`. Они не содержат токены или содержимое локальных файлов.

## Проверка и видео

`Tests/AgentCLIContractTests.swift` запускает настоящий собранный CLI, существующий socket/обработчик/очередь и временный локальный MTP-симулятор. Проверены отключение, хранилища, корень/вложенный/пустой каталог, Unicode, пагинация, неверные пути/ID, отказ доступа, устаревшее поколение подключения, JSON-типы, план/ожидание/итог и отсутствие записи до согласия. `AgentBridgeTests` дополнительно проверяет повтор, однократное использование плана, изменение источника, журнал и остановку сервиса.

Для записи видео на настоящем Switch сначала выполните только status → корень → вложенная папка через CLI. Затем согласуйте безопасный тестовый файл и папку, выполните plan → пользовательское подтверждение → operation.get. Аппаратный запуск и установка на Switch этой автоматической проверкой **не подтверждены**. Не показывайте симулятор как физическое устройство.

Переносимая короткая инструкция: [AGENT_INSTRUCTIONS.md](AGENT_INSTRUCTIONS.md). Детали прямого v1 socket: [AGENT_BRIDGE.md](AGENT_BRIDGE.md).
