Skip to content

CLI Overview

The poe-tiles CLI lets you manage and publish experiences on Poe Tiles from your terminal or in automated scripts. It wraps the REST API with a convenient interface, similar to gh for GitHub.

If you'd like to interface with the platform programmatically from code, see Programmatic Usage or the REST API docs.

Installing the CLI

The CLI requires Bun — its entry points are shipped as TypeScript and run by Bun directly.

Installing from a published tarball

Globally install the published tarball:

bash
bun remove -g @ai-app/poe-tiles-cli 2>/dev/null; bun install -g https://misc-dev-uploads.quora-913.workers.dev/dcd528d7f6bf68ef411de22cbd99b55e3d9ce9f634be8e76b0300869c40f8e75

This adds poe-tiles to Bun's global bin dir (~/.bun/bin). Ensure that directory is on your PATH, then verify:

bash
poe-tiles --version

The URL above points at the most recent tarball uploaded to misc-dev-uploads. It is regenerated when a new CLI version is published. The tarball is self-contained: production dependencies are plain npm packages and there are no workspace:* references. packaging.test.ts enforces this invariant — shipped source files may only import declared deps or node:* builtins.

Upgrading the CLI

Once installed, the CLI can upgrade itself in place:

bash
poe-tiles upgrade

This fetches the latest tarball manifest from https://poe-tiles-docs.pages.dev/cli/latest.json, runs bun remove -g @ai-app/poe-tiles-cli, then bun install -g <tarballUrl>. The fetched URL is pinned to the misc-dev-uploads.quora-913.workers.dev host before being passed to bun install -g, so a compromised docs deploy can't redirect installs to an attacker-controlled tarball. Only tarballUrl and publishedAt are read from the manifest — the install command is constructed CLI-side, never sh -c'd from a network string.

Dry run (print the equivalent install command without executing):

bash
poe-tiles upgrade --print

upgrade updates the globally-installed package in ~/.bun/bin.

Upgrading the SDK in an existing app

Separate from the CLI itself: scaffolded apps pin poe-tiles-sdk in their package.json to a content-hashed tarball URL, frozen at scaffold time. The docs and skills always describe the latest SDK, so bump the pin when a documented API is missing from your app. Replace the dependency value with the current tarball:

json
"poe-tiles-sdk": "https://misc-dev-uploads.quora-913.workers.dev/61d4eecd35145e330af985ad5dcc39d0cbf29ceab40b9b8071ab865956d2045e"

Then reinstall and sanity-check:

bash
bun install && poe-tiles doctor

This snippet is regenerated on every SDK release (same mechanism as the CLI install snippet above), so it always shows the current URL. You can also fetch it from the command line — poe-tiles sdk-url prints the current tarball URL (from the live release manifest, falling back to the URL baked into your CLI install when offline), and poe-tiles doctor warns when your app's poe-tiles-sdk pin differs from the SDK your CLI shipped with (run poe-tiles upgrade first so the comparison is against a current CLI). If your app predates the platform rename, work through Migrating a pre-rename app first.

Checking the version

bash
poe-tiles --version

Authentication

Run the browser login flow once to create a local session token:

bash
poe-tiles login

The CLI opens Poe in your browser, waits for approval, and saves ~/.poe-tiles/poe-tiles-session-token. You do not need a Poe API key.

On a remote machine or SSH session, run the same command. The CLI prints a URL and code; open the URL in any browser, approve the login, and the terminal keeps waiting until the session is saved. You can also force that flow:

bash
poe-tiles login --remote

For automation, pass an existing Poe Tiles session token:

bash
export POE_TILES_SESSION_TOKEN=your_session_token_here

Legacy Poe API keys still work via --api-key, POE_API_KEY, or ~/.poe-tiles/poe-tiles-api-key, but they are not needed for normal CLI use. Credential precedence is --api-key, then POE_API_KEY, then POE_TILES_SESSION_TOKEN, then ~/.poe-tiles/poe-tiles-session-token, then ~/.poe-tiles/poe-tiles-api-key.

Verify it works:

bash
poe-tiles auth whoami

Setup Doctor

Before building or publishing a generated tile workspace, run:

bash
poe-tiles doctor
poe-tiles doctor --cwd ./my-app

The doctor checks Bun, dependencies, .poe-tile.json, package scripts, the app's poe-tiles-sdk pin (warning when it differs from the SDK your CLI shipped with — see Upgrading the SDK in an existing app), and local publish auth. If it reports missing auth, run poe-tiles login and wait for browser approval; do not ask first-time creators to paste API keys into chat.

Available Commands

remix — Clone Published Source

bash
poe-tiles remix <typeId>
poe-tiles remix <creatorName> <handle>
poe-tiles remix <creatorName>/<handle> --dir ./my-local-copy

Downloads the app's uploaded source bundle and unpacks it into a local directory. By default the directory is named after the app handle; pass --dir <path> to choose a different local folder name. The target directory must not already exist.

After unpacking, remix rewrites the poe-tiles-sdk dependency to the published SDK tarball URL (so bun install works) and installs the SDK skills into .claude/skills/. Pass --no-skills to skip the skills. Any other internal workspace dependencies are left intact with a warning — they have no published package, so remove or replace them before bun install.

auth — Authentication & Account Info

bash
poe-tiles login                   # Open or print Poe login URL, save CLI session
poe-tiles auth whoami             # Show current user
poe-tiles auth usage              # Show point balance
poe-tiles auth points-history     # Show usage history table
poe-tiles auth logout             # End session

apps — App Management

bash
poe-tiles tiles list               # List all your apps
poe-tiles tiles get <tileHandleOrId>
poe-tiles tiles download-source <tileHandleOrId> --out source-bundle.tar.gz
poe-tiles tiles publish --handle my-app --dir ./dist
poe-tiles tiles publish --change-summary "Added touch controls and improved scoring."
poe-tiles tiles rename <tileHandleOrId> <newHandle>

tiles download-source — Download uploaded source

Downloads the source-code bundle attached to a published app and writes it as a .tar.gz archive. Accepts either the canonical app ID or userHandle/appHandle.

bash
poe-tiles tiles download-source myuser/my-app --out source-bundle.tar.gz

This only works when the app was published with a source bundle. Apps scaffolded by poe-tiles tiles init include a public sourceBundle block in .poe-tile.json by default, so normal publishes upload source automatically. Older apps or apps that deleted the sourceBundle block cannot have original source reconstructed from the deployed runtime bundle.

tiles rename — Rename one of your apps

Rewrites the app's handle without re-publishing the bundle. Accepts either the canonical app ID or userHandle/appHandle. New handle must be unique across your apps.

tiles publish — App-page metadata flags

tiles publish accepts optional metadata fields that decorate the app's landing page and the cards in app listings. Each can be set in .poe-tile.json or overridden by a CLI flag (explicit flag wins). Pass an empty value (--profile-picture="", etc.) to clear an existing value on re-publish.

Flag.poe-tile.json fieldNotes
--change-summary <text>(none; per-publish)Concise summary included in Creator's chat receipt (≤500 chars). Whitespace is normalized; omission falls back to “New version published.”
--display-name <name>displayNameHuman-friendly name shown on Explore and app cards instead of the kebab-case handle (≤80 chars).
--profile-picture <pathOrUrl>profilePictureLocal PNG/JPG/WEBP file path (uploaded, ≤512 KB) or an https:// URL stored as-is.
--short-description <text>shortDescriptionOne-line tagline (≤140 chars).
--long-description <path>longDescriptionPath to a markdown file (≤16 KB UTF-8). The CLI reads the file contents on publish.
(config only — no flag)screenshotsOrdered list of ≤8 detail-card screenshots (local PNG/JPG/WEBP paths or https:// URLs). Requires $schema v3+.
(config only — no flag)playersPlayer-count facet { min, max?, recommended? } (total seats: humans + AI), or null to clear. Requires $schema v5. Shown on tile cards and the detail page.
(config only — no flag)instancing"per-context" reuses one instance per context; "per-launch" creates a fresh instance for each launch. Requires $schema v7.

These fields are gated by the .poe-tile.json $schema version: the metadata text fields need v2.json+, screenshots needs v3.json+, and players needs v5.json, and instancing needs v7.json. Update the $schema value in your config to the newest version to use the latest fields; using a field under an older schema is a loud error, never a silent drop. The previous v1 schema is rejected by the current CLI.

players example:

jsonc
{
  "$schema": "https://poe-tiles-docs.pages.dev/schemas/poe-tile/v7.json",
  "handle": "word-duel",
  "players": { "min": 2, "max": 4, "recommended": { "min": 3, "max": 4 } },
  "instancing": "per-launch"
}

max is optional — omit it for an open/party tile with no declared upper bound. recommended is an optional { min, max } sub-range bounded by [min, max ?? 1000]. Set "players": null to un-declare a previously published value.

tiles publish — Visibility

tiles publish --visibility public|unlisted|dev controls whether the app appears in public catalog surfaces. Omit the flag to create public apps by default; on re-publish, omission preserves the current visibility. unlisted apps keep their direct /apps/<user>/<handle> links but are excluded from Explore, For You, search for non-owners, and public profile app lists. dev is an internal/legacy visibility still accepted by the publish plumbing; prefer public or unlisted for new apps. It is unrelated to the is_dev_app flag, which has different semantics.

stores — Store Operations

bash
poe-tiles stores access <typeId> <instanceId>
poe-tiles stores schema <typeId> <instanceId>
poe-tiles stores mutate <typeId> <instanceId> <mutationName> --input '{"id":"todo-1","text":"Buy milk"}'
poe-tiles stores mutate <typeId> <instanceId> <mutationName> --input-file input.json

Store commands use the authenticated user from poe-tiles login (or a legacy API key fallback) and the normal user-router WebSocket.

stores access verifies access by pulling the target store and checking that the current user's id is present as an active row in the store's $users table. It exits non-zero when the user is not an active member.

stores schema fetches the store schema, including all available mutators, and prints ready-to-run CLI commands for checking access and dispatching each mutator.

stores mutate fetches the store schema version, then sends a single mutation push. It exits non-zero when the server reports a failed mutation.

Use --input <json> for inline JSON or --input-file <path> for a JSON file. Pass exactly one of those flags. For a mutator that takes no meaningful input, pass --input null or --input '{}', depending on the mutator's schema.

chat — Interactive AI Chat

bash
poe-tiles chat                    # Start interactive chat (default: Claude-Opus-4.6)
poe-tiles chat --model GPT-4o    # Use a different model

models — Model Management

bash
poe-tiles models list             # List available LLM models

accounts — Connected Accounts

bash
poe-tiles accounts list           # List linked OAuth accounts
poe-tiles accounts unlink <providerId>

upgrade — Self-Upgrade

Fetches the latest tarball manifest and reinstalls the global CLI. See Upgrading the CLI above for security and scope notes.

bash
poe-tiles upgrade           # Upgrade to latest published tarball
poe-tiles upgrade --print   # Print the install command without running it

skills — SDK Skills

Install (or refresh) the SDK skills (tile-creator, synced-store) bundled with the currently-installed CLI. Use this to pull in newer skill content after upgrading the CLI, without re-scaffolding the app.

bash
poe-tiles skills install                        # Writes to .claude/skills (default)
poe-tiles skills install --dir path/to/skills   # Custom target dir

Merges with existing skill directories — files at matching paths are overwritten, but user-added files outside the bundle are preserved. Each skill is written as <dir>/<skill-name>/. Note: a reference doc that was renamed or removed in a newer CLI will linger alongside the new version until you delete it manually.

Global Options

bash
--json                           # Output as machine-readable JSON
--server <url>                   # Override the server URL (e.g. for local dev)
--api-key <key>                  # Legacy Poe API key override
--version                        # Show version
--help                           # Show help

Environment Variables

VariableRequiredDescription
POE_TILES_SESSION_TOKENNoExisting Poe Tiles session token; normally created by poe-tiles login in ~/.poe-tiles/poe-tiles-session-token
POE_API_KEYNoLegacy Poe API key (get one here); explicit API keys take precedence over session-token auth
POE_SERVER_URLNoServer URL override (same as --server flag)

Using in scripts

All commands support --json for machine-readable output, making it easy to pipe into tools like jq:

bash
# List app handles
poe-tiles tiles list --json | jq '.[].handle'

# Sum point costs
poe-tiles auth points-history --json | jq '[.[] | .cost_points] | add'

# Get a specific app's bundle URL
poe-tiles tiles get myuser/my-app --json | jq '.bundleUrl'

# Dispatch a store mutation and read the committed version
poe-tiles --json stores mutate todo-store room-1 setTodo --input '{"id":"todo-1","text":"Buy milk"}' | jq '.version'

# List mutators and their generated CLI commands
poe-tiles --json stores schema todo-store room-1 | jq '.commands.mutators'

Local development

Point the CLI at a local server:

bash
poe-tiles --server http://localhost:8787 tiles list

Programmatic usage

The CLI package also exports a SlopPoeClient for use in scripts and tools:

typescript
import { SlopPoeClient, assertSuccess } from "@ai-app/poe-tiles-cli";

const client = new SlopPoeClient({
  sessionToken: process.env["POE_TILES_SESSION_TOKEN"]!,
});

const result = await client.listApps();
assertSuccess(result); // throws on failure
for (const app of result.apps) {
  console.log(`${app.handle} (${app.id})`);
}