Appearance
CLI Overview
The poe-tiles CLI lets you manage and publish experiences on Joiner 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
For a new installation, run the current global-install command from the getting-started guide. For an existing installation, use poe-tiles upgrade as described below. Do not reuse a CLI tarball URL from an older skill copy; bundled skills cannot embed their own content-hashed release URL.
This adds poe-tiles to Bun's global bin dir (~/.bun/bin). Ensure that directory is on your PATH, then verify:
bash
poe-tiles --versionThe 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 upgradeThis 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 --printupgrade updates the globally-installed package in ~/.bun/bin.
Upgrading the SDK in an existing activity
Scaffolded activities pin poe-tiles-sdk in package.json to a content-hashed tarball URL. Run poe-tiles sdk-url to get the current URL from the live release manifest, then replace the dependency value with that URL. When offline, the command falls back to the SDK URL bundled with the installed CLI. The release snapshot below shows the dependency format; a bundled skill copy can contain an older pin, so prefer the live result when upgrading:
json
"poe-tiles-sdk": "https://misc-dev-uploads.quora-913.workers.dev/3c7bdfb6803b2aef9cf40e281d98eb09825dc006cd68cafcd14348dc50c62cd4"Then reinstall and sanity-check:
bash
bun install && poe-tiles doctorpoe-tiles doctor warns when the activity's SDK pin differs from the SDK bundled with the CLI. Run poe-tiles upgrade first so that comparison uses a current CLI. For an activity predating the platform rename, follow the pre-rename migration guide first.
Checking the version
bash
poe-tiles --versionAuthentication
Run the browser login flow once to create a local session token:
bash
poe-tiles loginThe CLI opens Joiner 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 --remoteFor automation, pass an existing Joiner session token:
bash
export POE_TILES_SESSION_TOKEN=your_session_token_hereLegacy 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 whoamiSetup Doctor
Before building or publishing a generated activity workspace, run:
bash
poe-tiles doctor
poe-tiles doctor --cwd ./my-appThe 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-copyDownloads 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.
It also records where the copy came from, by writing a remixOf field into the unpacked .poe-tile.json (and upgrading its $schema to v10). It also removes obsolete top-level creator categories from the copy, without changing the stored archive. You don't need to write or edit this yourself. Once you publish, it shows the original app in a Remix of row on your detail card.
The claim is not verified — it is a credit you declare. The platform checks only that the field is well-formed; it does not look up the app you named. If your app is public, it also appears in a Remixes row on the original app's detail card (unlisted and dev apps never do). If the app you name is deleted, private, or was never real, the row simply doesn't render. Set "remixOf": null to drop the credit on a later publish.
If the server didn't report the branch point, remix still gives you a working copy — it just reports that the credit wasn't recorded instead of failing.
auth — Authentication & Account Info
bash
poe-tiles login # Open or print browser approval 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 sessionapps — 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>Version management
Every publish appends a new version. staging always advances to the newest publish; whether live (the version everyone plays) also advances is the activity's Auto-Advance Live preference (on by default).
bash
poe-tiles tiles versions <tileHandleOrId> # List versions: names, descriptions, LIVE/STAGING markers
poe-tiles tiles channels <tileHandleOrId> # Which version live/staging point at
poe-tiles tiles promote <tileHandleOrId> <version> # Set a version live (or roll back)
poe-tiles tiles set-channel <tileHandleOrId> staging <version> # Point staging at a version
poe-tiles tiles auto-advance <tileHandleOrId> on|off # Should each publish go live immediately?
poe-tiles tiles edit-version <tileHandleOrId> <version> --name "Beta 2" --description "Adds the timer."
poe-tiles tiles publish --version-name "Beta 2" --version-notes "Adds the timer."A publish that omits --version-notes but passes --change-summary stores the summary as the version's description, so summarized publishes get self-documenting history for free. edit-version accepts an empty string (--name "") to clear a field back to its default.
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.gzThis 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 — Output
A successful publish prints the activity's ids, its bundle ids, and its Tile URL. When the activity's declared players range makes it worth testing with more than one person, one more row appears:
Multiplayer simulation (4 players) https://<host>/simulate_multi/start?typeId=<id>&users=4Opening that URL starts (or re-enters) a multiplayer simulation of the activity: one page holding several phone-sized frames, each running the full product as a different dev user, all already members of the same instance of your activity. It is the fastest way to exercise turn order, live opponent updates, and the room-side notification surfaces without recruiting real players. Repeat visits re-enter the same simulation rather than piling up new ones, and the simulation's own "Add user" control adds more phones.
The phone count comes from players: recommended.min when declared, otherwise min, clamped to a screen-friendly maximum. Append &users=<n> to ask for a different number — on a reused simulation the count is a floor, not an exact size: already-seated phones are never removed, so asking for fewer than a previous visit still opens the larger set (close individual phones in the view instead). The link is absent for a single-player activity and for an activity that declares no players at all — the platform never guesses.
In --json mode the same pair is a simulation object ({ url, users }), omitted entirely when there is nothing to simulate.
The link is also posted into the Creator chat automatically alongside the publish receipt, so it is reachable without scrolling back through terminal output.
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 field | Notes |
|---|---|---|
--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.” |
--version-name <name> | (none; per-publish) | Label for the new version (≤60 chars), shown in the version-history UI instead of the default "Version N". |
--version-notes <text> | (none; per-publish) | What changed in this version (≤2000 chars), stored on the version row. Defaults to --change-summary when omitted. |
--display-name <name> | displayName | Human-friendly name shown on Explore and app cards instead of the kebab-case handle (≤80 chars). |
--profile-picture <pathOrUrl> | profilePicture | Local PNG/JPG/WEBP file path (uploaded, ≤512 KB) or an https:// URL stored as-is. |
--short-description <text> | shortDescription | One-line tagline (≤140 chars). |
--long-description <path> | longDescription | Path to a markdown file (≤16 KB UTF-8). The CLI reads the file contents on publish. |
| (config only — no flag) | screenshots | Ordered list of ≤8 detail-card screenshots (local PNG/JPG/WEBP paths or https:// URLs). Requires $schema v3+. |
| (config only — no flag) | players | Player-count facet { min, max?, recommended? } (total seats: humans + AI), or null to clear. Requires $schema v5. Shown on activity 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. |
| (config only — no flag) | remixOf | The app — and the exact publish of it — this app was remixed from, or null to clear. Requires $schema v9. poe-tiles remix writes this for you; see below. Shows the original in a "Remix of" row on your detail card, and lists a public app in a "Remixes" row on the original's. Self-declared, not verified. |
These fields are gated by the .poe-tile.json $schema version: the metadata text fields need v2.json+, screenshots needs v3.json+, players needs v5.json, instancing needs v7.json, and remixOf needs v9.json. The current schema is v10.json; categories are no longer creator inputs. 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 + instancing example:
jsonc
{
"$schema": "https://poe-tiles-docs.pages.dev/schemas/poe-tile/v10.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 activity 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.
Categories are managed by the platform, not by creators. The current CLI rejects any top-level categories field, including null and an empty list, under every accepted config schema. Fix a legacy config by deleting that field:
Remove the top-level
categoriesfield from.poe-tile.json; categories are assigned automatically.
Keep the rest of the config intact. Remix and fresh source-copy preparation remove this obsolete field automatically; existing or restored workspaces need the edit above.
tiles visibility — Discovery
tiles visibility <tileHandleOrId> public|unlisted controls whether the activity appears in public discovery. Publishing never sets it: an activity is unlisted when first published and stays where it is on every publish after that, so listing (or unlisting) is always a separate, deliberate command.
An unlisted activity keeps its direct /tile/<user>/<handle> link but is excluded from Explore, For You, search for non-owners, and public profile activity lists. Accepts either the canonical activity ID or userHandle/tileHandle.
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
poe-tiles store files upload <typeId> <instanceId> ./photo.jpg --content-type image/jpegStore 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.
store files upload runs the managed upload pipeline for an active store member and prints the published fileId, content key, and serving URL. The app must opt in with _requestUploadUrl. Claim the returned fileId in a fileRefs-declared field through store mutate; unclaimed uploads expire. The file must be non-empty and at most 10 MiB. Use --name to override the display name and --content-type to provide a receiver hint; the platform inspects the bytes authoritatively.
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.
models — Model Management
bash
poe-tiles models list # List available LLM modelsaccounts — 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 itskills — 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 dirMerges 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 helpEnvironment Variables
| Variable | Required | Description |
|---|---|---|
POE_TILES_SESSION_TOKEN | No | Existing Joiner session token; normally created by poe-tiles login in ~/.poe-tiles/poe-tiles-session-token |
POE_API_KEY | No | Legacy Poe API key (get one here); explicit API keys take precedence over session-token auth |
POE_SERVER_URL | No | Server 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 listProgrammatic 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})`);
}