Search by

knlab / pb-migrate

knlab

CLI tool to sync local AIML projects with Pandorabots

Package info

github.com/knlab/pb-migrate

Type:project

pkg:composer/knlab/pb-migrate

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.7.4 2026-10-03 15:32 UTC

This package is auto-updated.

Last update: 2026-10-03 15:34:05 UTC


README

日本語版 / Japanese

Command-line tool to manage existing AIML packages on Pandorabots, built on top of spontena/pb-php.

pb-migrate is the OSS rewrite of an in-house deployment CLI. The tool treats your local AIML files as the source of truth, registers existing package directories, and pushes them to Pandorabots' Developer Portal API.

Concepts

  • Local registration is the source of truth. A bot must be registered locally (pb-migrate add) before it can be operated on remotely. Pandorabots-side state never authoritatively drives the local config.
  • One project = one app_id. A pb-migrate.json file describes a project; all bots under it share one Pandorabots application ID. Multi-app_id setups are out of scope.
  • Credentials live in .env, structure in pb-migrate.json. The JSON file is committable; the .env is gitignored. Both can be edited by the tool.
  • Default push is destructive. It rewrites the remote bot to match local. Pass --keep-remote-only to opt out.

Features

  • add / remove — register or unregister an existing AIML package directory
  • config — interactively edit credentials (.env) for project or per-bot bot_keys
  • bot:list — show registered bots (local, no API call)
  • bot:remote — show bots on the Pandorabots account, annotated with registration state
  • bot:create / bot:delete / bot:files — remote bot lifecycle (require local registration)
  • compile — verify bots on Pandorabots
  • push — upload local files (destructive: rewrites remote to match local)
  • pull — download remote files into the local directory
  • diff — show file-level changes (UPD/ADD/DEL grouped, color-coded)
  • report — rich handoff report of pending changes
  • status — local sync state vs. cache: files added, edited or deleted since the last push / pull (no API)
  • cat / file:delete — inspect or delete a single remote file
  • talk / debug / atalk — converse with a bot from the terminal
  • test — assert bot replies match expected outputs
  • batch — run a runbook of pb-migrate commands
  • alter:list / alter:set / alter:unset / alter:reset — persistent file-body overrides for debug-session probes
  • Run pb-migrate with no arguments to drop into an interactive REPL

Requirements

  • PHP 8.1+
  • ext-json
  • Composer

Installation

Install globally:

composer global require knlab/pb-migrate

Make sure ~/.composer/vendor/bin (or the equivalent for your Composer setup) is on your $PATH.

Or pin it inside a project:

composer require --dev knlab/pb-migrate
./vendor/bin/pb-migrate --version

Quickstart

mkdir -p ~/work/my-bots/aiml/mybot && cd ~/work/my-bots
$EDITOR aiml/mybot/greetings.aiml          # write your AIML

pb-migrate add ./aiml/mybot                # registers `mybot` in pb-migrate.json
pb-migrate config                          # prompts for PB_APP_ID / PB_USER_KEY → writes .env

pb-migrate bot:create mybot                # creates the bot on Pandorabots
pb-migrate push --bot mybot                # uploads aiml/mybot/* and compiles
pb-migrate talk hello --bot mybot
pb-migrate                                 # drop into REPL

Configuration

pb-migrate.json holds the project structure (no credentials):

{
  "$schema": "https://knlab.github.io/pb-migrate/schema.json",
  "bots": {
    "mybot": { "directory": "./aiml/mybot" },
    "other": { "directory": "./aiml/other", "propertiesUpload": "full" }
  }
}

The $schema URL points at the published JSON Schema. VS Code, JetBrains IDEs, and most JSON-aware editors pick it up automatically — autocomplete on field names, hovers explaining what each field does, and instant warnings on typos like directry or invalid propertiesUpload values.

Per-bot fields:

Field Required Default Notes
directory yes — Path to the AIML package directory; relative to project root
propertiesUpload no additive full to delete remote properties before re-uploading (strict GitOps)
alters no {} Map of canonical name → override file path (debug-session probes; managed by alter:* commands)

Bot names must be alphanumeric (Pandorabots constraint).

Credentials (.env, tool-managed)

The tool writes a project-local .env (gitignored) using block markers so user-managed lines are preserved:

# pb-migrate:begin app
PB_APP_ID=12345678abcdef
PB_USER_KEY=xxxxxxxxxxxxxxxxxxxx
# pb-migrate:end app

# pb-migrate:begin bot=secretbot
PB_BOT_SECRETBOT_KEY=zzzzzzzzzzzzzzzzz
# pb-migrate:end bot=secretbot

# user-managed lines below this line are preserved on every tool write
MY_OWN_VAR=foo
Variable Purpose
PB_APP_ID Pandorabots application ID (required)
PB_USER_KEY Pandorabots user key (required)
PB_HOST API host. Defaults to https://api.pandorabots.com
PB_BOT_<UPPER_BOTNAME>_KEY Per-bot bot_key for atalk. Only the bots that need anonymous talk have one.

Edit via:

pb-migrate config                                  # prompts for project credentials
pb-migrate config --app-id X --user-key Y          # CI / scripted form
pb-migrate config --bot mybot                      # prompts for that bot's bot_key
pb-migrate config --bot mybot --bot-key VALUE
pb-migrate config --show                           # display all values

The interactive prompts show the current value; press Enter to keep it, type a new value to update, or type - to clear an optional field.

A note on atalk and bot_keys

atalk (anonymous talk) requires a per-bot bot_key. Pandorabots issues bot_keys through their dashboard UI (developer.pandorabots.com), not through the Developer Portal API.

Heads-up: bots created via the API (pb-migrate bot:create, or anything else hitting PUT /bot/...) do not appear in the dashboard's bot list during the API trial / Developer Portal tier we tested against. There seems to be no API endpoint that returns the bot_key either. The practical consequence: if you want atalk to work, create the bot on the dashboard first and pb-migrate add it locally, rather than using bot:create to spin it up via the API.

talk and debug use the regular user_key authentication and work fine for either path.

Commands

add <directory> [--bot <name>] [--force]    Register a package directory
remove <botname> [--yes]                    Unregister a bot (does not touch remote)
config [--bot <name>] [--show]              Edit credentials in .env
       [--app-id X --user-key Y]
       [--bot-key Z]
bot:list                                    List registered bots (local, no API)
bot:remote                                  List bots on the Pandorabots account, annotated
bot:create <botname>                        Create a registered bot on Pandorabots
bot:delete <botname> [--yes]                Delete a bot on Pandorabots
bot:files --bot <botname>                   List files stored on a single bot
compile [--bot ...|--all]                   Compile (verify) one or more bots
push  [--bot ...|--all] [--dry-run]         Push local files to bot(s); destructive by default
                        [--skip-compile]    (rewrites remote to match local; --keep-remote-only to opt out)
                        [--keep-remote-only]
                        [--verify-remote]
                        [--only=...]
                        [--override n=p]
                        [-i|--interactive]
                        [--properties-upload=additive|full]
pull  [--bot ...|--all] [--only=...]        Pull bot files to the local directory
                        [--force]           (skips locally edited files; --force to overwrite)
diff  [--bot ...|--all] [--verify-remote]   File-level UPD/ADD/DEL grouped diff
                        [--only=...]
status [--bot ...|--all]                    Local sync state of registered bots (no API)
report [--bot ...|--all] [--verify-remote]  Rich handoff report of pending changes
                        [--only=...]
                        [--since=remote|cache]
                        [--utf8-borders]
test   [--bot ...|--all]                    Assert bot replies match expected; silent on success
       --input X --expect Y                 inline test, OR
       --file tests.txt                     <input>|<expected> per line
       [--show-pass]                        also print PASS lines
cat [<name>] --bot --kind                   Print a single remote file body to stdout
file:delete [<name>] --bot --kind [--yes]   Delete a single remote file (omit name for pdefaults / properties)
batch <runbook.txt> [--continue-on-error]   Execute a list of pb-migrate commands from a file
                    [--echo]
talk  <input> --bot <botname>               Talk to a bot
debug <input> --bot <botname> [--json]      Talk with trace; default formatted, --json for raw
atalk <input> --bot <botname>               Anonymous talk via per-bot bot_key
alter:list [--bot ...|--all]                List persistent alters (defaults to --all)
alter:set <name> <path> --bot <bot>         Add or update a persistent alter
alter:unset <name> --bot <bot>              Remove a single persistent alter
alter:reset --bot <bot> [--yes]             Wipe every alter on a bot
repl                                        Interactive shell (default)

For --bot, a glob pattern (prod.*) is accepted in addition to an exact bot name. --all operates on every registered bot.

--only takes a comma-separated list of file names (greet), kind/name pairs (file/greet, set/colors), or the bare kind for the two kinds that have no name of their own (properties, pdefaults).

Push semantics

push is destructive by default: it rewrites the remote bot to match local. Files that exist on the remote but not locally are deleted. This matches the "local is source of truth" model — what's on disk is what should be on the bot.

To preserve remote-only files (e.g. files added via the Pandorabots dashboard by other team members), pass --keep-remote-only.

Pandorabots-managed files like udc are never deletable (412 from the API); pb-migrate skips them with a warning regardless of mode.

Pull semantics

pull writes remote files into the local directory, but it will not silently discard work you have not pushed yet. For each file it compares the local SHA-256 with the hash recorded in the local cache at the last successful push or pull:

Local file Cache entry Result
hash matches cache yes overwritten (no local edits since last sync)
hash differs from cache yes skipped with a warning — you edited it locally
exists none overwritten (first pull, or a file dropped in manually)
missing — written
mybot:
  skip file/greet — local file edited since last sync (use --force to overwrite)
  1 file(s) skipped due to local edits

Pass --force to overwrite regardless. If you want the remote version but also want to keep your edits, diff --bot mybot first, or commit the local directory before pulling.

Local files may live in subdirectories of the bot directory (ja/greet.aiml); Pandorabots' namespace is flat, so the subdirectory is purely a local convention. pull writes into the existing path when a file with the same kind and name is already present, and only falls back to the directory root for files that do not exist locally yet. The arrow in the output shows where each file landed:

mybot:
  ↓ file/greet → ja/greet.aiml
  ↓ file/farewell → farewell.aiml

Files the server refuses to serve (HTTP 4xx on system-managed files such as udc) are skipped with the server's message — see Error handling.

Diff and report

diff shows file-level changes only — which files differ, in what category — without inline content diffs:

mybot:
URL: https://api.pandorabots.com
BOT: app-x/mybot

UPD(1):
    file/greet
ADD(1):
    file/farewell
DEL(1):
    file/oldfile

report produces a richer document suitable for handoff notes / PR descriptions:

============================================================
Pending changes for bot mybot
============================================================
Generated: 2026-05-04 18:30 (--since=remote)

--- Updates (1) ----------------------------------------
  file/greet  (3.1 KB)

--- Additions (1) --------------------------------------
  file/farewell  (0.8 KB)

--- Removals (1) ---------------------------------------
  file/oldfile  (remote-only)

--- Summary ----------------------------------------
  1 added, 1 updated, 1 remote-only
  Total local size: 3.9 KB

Both default to comparing against live remote. Pass --since=cache on report to compare against the local cache instead (no API call) — useful for seeing what changed locally since the last push/pull.

Persistent alters (debug-session probes)

--override is great for one-shot tweaks. For longer investigative sessions where you want a few debug probes (a category that dumps internal predicates, one that simulates state, etc.) to re-apply on every push, register them as persistent alters:

pb-migrate alter:set _dump_predicates variants/dump-predicates.aiml --bot mybot
pb-migrate alter:set greet variants/greet-debug.aiml --bot mybot

pb-migrate push --bot mybot   # warns "2 active alter(s) detected"
# … iterate, talk to bot, observe …

pb-migrate alter:reset --bot mybot   # clear all probes before production push
pb-migrate push --bot mybot

alter:list flags missing override paths with [missing!] so a session in flight is visible at a glance.

⚠️ Persistent alters live in pb-migrate.json, which is typically committed. Run alter:reset before merging your config back into a shared branch — alters are meant to leave no trace once the debug session is over.

Local cache

To avoid re-fetching every remote file on every push / diff, pb-migrate maintains a JSON cache (.pb-migrate-cache.json, gitignored) of the SHA-256 of each file at the time of the last successful push or pull. Pass --verify-remote to bypass the cache when you suspect dashboard edits or cache corruption.

Error handling

API failures surface as Spontena\PbPhp\Exception\ApiException. Since pb-php 2.1.4 its message carries only the HTTP status and the request method/path — never the query string (which holds user_key / botkey) and never the raw response body. pb-migrate re-attaches the server's own explanation (the message field of the JSON error body) wherever an error is shown, so you still see why a call failed:

# REPL, batch, test
Pandorabots API returned HTTP 404 for GET https://api.pandorabots.com/bot/<app_id>/mybot — bot not found

# direct CLI run (Symfony error block, then the server message)
  Pandorabots API returned HTTP 404 for GET https://api.pandorabots.com/bot/<app_id>/mybot

Server message: bot not found

# push / pull skip lines
  skip file/udc — server returned HTTP 404: not found (likely a system-managed file)
  skip file/udc — server returned HTTP 412: system file cannot be deleted (system-managed file)

When the body carries no usable message (empty, non-JSON, or a non-string field), the line is shown without the trailing detail. Transport failures (DNS, timeout, TLS) keep the standard Guzzle exception types with a fixed message and the numeric cURL error code; the original URL-bearing message is intentionally dropped.

Exception messages, stack traces, and the -v / -vvv output never include credentials. If you log exception traces yourself on PHP 8.1, set zend.exception_ignore_args=On; on PHP 8.2+ pb-php marks its sensitive parameters with #[\SensitiveParameter]. Do not dump whole exception objects or Guzzle request/response accessors — those can still contain the raw query string or conversation text.

Testing the code

composer install
composer test                    # PHPUnit unit suite (mocked HTTP)
composer analyse                 # PHPStan level 6

The integration suite hits the real Pandorabots API and is not run by default. Provide credentials and select the integration suite explicitly:

PB_APP_ID=xxx PB_USER_KEY=yyy composer test:integration

CI runs the unit suite on PHP 8.1 / 8.2 / 8.3 / 8.4 — see .github/workflows/ci.yml.

License

MIT — see LICENSE.