Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 66 additions & 21 deletions .agents/skills/openclaw-pixellab-avatar/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ Ask the user:
At minimum include `idle` and `thinking` (the watch auto-plays `thinking`
while waiting for replies).
- **Agent id** this will be wired into (default: `agent`).
- **Voice** — which ElevenLabs voice the agent should speak with. Offer
to run `--list-voices` (see Step 5b) so the user can pick; if they have
no preference, suggest `--voice auto` for a smoke-test voice they can
change later. A voice id drops an `elevenlabs` voice block into the
wired config so TTS just works the moment the gateway restarts.

### 2. Create the character (step 1 of 4)

Expand Down Expand Up @@ -94,16 +99,44 @@ Expect ~5–10 min per emotion. The script prints `✓ <emotion> complete` as
each job finishes. If any emotion fails, the script exits non-zero with a
summary — rerun with just the failed emotions to fill in the gaps.

### 5. Export into SpriteCore (step 4 of 4)
### 5a. (Optional) Discover ElevenLabs voices

Before exporting, if the user wants to pin a specific voice, list what's
in their ElevenLabs library:

```bash
node scripts/pixellab-export.mjs --list-voices
```

No `--uid` required — this is a read-only lookup. Each line prints
`<voiceId> <name> [category] <labels>`. Copy the voice id the user
picks and pass it to the export step via `--voice-id`.

Auth: `ELEVENLABS_API_KEY` env, `--elevenlabs-api-key-command <cmd>`,
or `pass show elevenlabs/api-key`.

### 5b. Export into SpriteCore (step 4 of 4)

Once animations exist on the character, the exporter pulls the ZIP bundle,
calls `/characters/<id>/animations` for canonical emotion names (`happy`,
`sad`, etc. — not the verbose pixellab slugs), packs frames into a WebP
atlas, writes the manifest, and prints the config snippet ready to paste
into `openclaw.json`.
atlas, writes the manifest, generates the config block (including the
voice if a voice id was supplied), and with `--apply` patches
`openclaw.json` directly.

```bash
# Writes atlas + manifest to ~/.openclaw/assets/avatars/<agent-id>/
# Normal path: write atlas + manifest AND patch openclaw.json in one shot.
# The exporter backs up the config before writing; restart the gateway
# afterward so it picks up the new agent entry.
node scripts/pixellab-export.mjs \
--uid <character_id> \
--agent-id <agent-id> \
--overwrite \
--apply

# Dry-style path: write atlas + manifest only; print the openclaw.json
# snippet for manual review/paste. Use this when you want to eyeball the
# block before wiring it in.
node scripts/pixellab-export.mjs \
--uid <character_id> \
--agent-id <agent-id> \
Expand Down Expand Up @@ -180,11 +213,16 @@ exporter, so any existing call site keeps working.

### 6. Wire into `openclaw.json`

Copy the config snippet the exporter prints into `openclaw.json` under
`plugins.entries["sprite-core"].config.agents.<agent-id>`, then restart the
gateway. Default state from the snippet is `idle` when present; otherwise
the first animation. Review before saving — sometimes you want a more
specific default.
If you ran the exporter with `--apply`, this step is already done — the
exporter wrote the agent block under
`plugins.entries["sprite-core"].config.agents.<agent-id>` and printed the
backup path. Skip to the restart in Step 7.

If you ran without `--apply`, copy the config snippet the exporter printed
into `openclaw.json` under
`plugins.entries["sprite-core"].config.agents.<agent-id>`. Default state
from the snippet is `idle` when present; otherwise the first animation.
Review before saving — sometimes you want a more specific default.

### 7. Verify

Expand All @@ -208,15 +246,22 @@ specific default.

## Open TODOs

- `--apply` flag for the exporter: patch the snippet directly into
`openclaw.json` under `plugins.entries["sprite-core"].config.agents.<id>`
and optionally restart the gateway, so the whole flow is one command.
- Animate-then-tag pipeline: today the operator passes `--rename` so the
exporter can collapse pixellab's verbose slugs back to canonical emotion
names. Better would be for `pixellab-animate.mjs` to tag each animation
upstream with the emotion key it was invoked with, making `--rename`
unnecessary.
- `/characters/<id>/animations` currently 404s in our environment so the
exporter falls back to slug names. Confirm the pixellab API contract
(endpoint path / required auth / response shape) and update the exporter,
or remove the fetch entirely if it's permanently gone.
- Animate-then-tag pipeline: today the exporter's `DEFAULT_CANONICAL_RENAMES`
map collapses pixellab's verbose slugs back to canonical emotion names
(`idle`, `happy`, `sad`, …) when `/characters/<id>/animations` 404s.
Better would be for `pixellab-animate.mjs` to tag each animation upstream
with the emotion key it was invoked with (via a name field in the
animate-character request or a follow-up PATCH), so the mapping is
upstream and the exporter never has to guess.
- Auto-restart the gateway after `--apply`: currently the exporter prints a
restart command but intentionally does not run it (visible side effect
the operator should own). Add `--restart-gateway` as opt-in sugar for
unattended runs.
- `/characters/<id>/animations` currently 404s in our environment; the
exporter falls back to slug names plus the canonical rename map.
Confirm the pixellab API contract (endpoint path / required auth /
response shape) and update the exporter, or remove the fetch entirely
if it's permanently gone.
- Pixellab 3-concurrent-job cap: account-wide limit seen during batch
runs. Future: add a simple semaphore to the animate or batch scripts so
parallel pipelines block-and-retry instead of 429'ing out on create.
100 changes: 100 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
# Fast "does the metadata parse" job that gates everything else.
metadata:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Validate root package.json
run: node -e "require('./package.json')"
- name: Validate plugin package.json + openclaw.plugin.json
run: |
node -e "require('./packages/plugin/package.json')"
node -e "require('./packages/plugin/openclaw.plugin.json')"
- name: Validate client-js package.json
run: node -e "require('./packages/client-js/package.json')"
- name: Validate schema package.json
run: node -e "require('./schema/package.json')"
- name: Validate fixture JSON
run: |
set -e
find fixtures -name '*.json' | while read f; do
node -e "JSON.parse(require('fs').readFileSync('$f', 'utf8'))"
done
- name: Verify plugin name matches install.npmSpec
working-directory: packages/plugin
run: |
node -e "
const pkg = require('./package.json');
const spec = pkg.openclaw?.install?.npmSpec;
if (spec !== pkg.name) {
console.error('Mismatch: name=' + pkg.name + ' vs install.npmSpec=' + spec);
process.exit(1);
}
"
- name: Verify version alignment across packages
run: node scripts/check-versions.mjs

typescript:
runs-on: ubuntu-latest
needs: metadata
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: pnpm/action-setup@v4
with:
version: 9
- name: pnpm install
run: pnpm install --frozen-lockfile
- name: Build schema + client-js
run: pnpm --filter ./schema --filter ./packages/client-js run --if-present build
- name: Test client-js
run: pnpm --filter ./packages/client-js test
- name: Typecheck client-js
run: pnpm --filter ./packages/client-js typecheck
# Plugin typecheck has pre-existing drift against the pinned openclaw
# version (see PR description). Skipped until that's sorted in a
# follow-up; uncomment once the plugin is re-aligned with the openclaw
# plugin-sdk it targets.
# - name: Typecheck plugin
# run: pnpm --filter ./packages/plugin typecheck

kotlin:
runs-on: ubuntu-latest
needs: metadata
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: gradle/actions/setup-gradle@v4
- name: Gradle build + test (:core, :android)
working-directory: packages/client-kotlin
run: gradle :core:build :core:test :android:build :android:test --no-daemon

swift:
runs-on: macos-latest
needs: metadata
steps:
- uses: actions/checkout@v4
- uses: swift-actions/setup-swift@v2
with:
swift-version: "5.10"
- name: swift build
working-directory: packages/client-swift
run: swift build
- name: swift test
working-directory: packages/client-swift
run: swift test
120 changes: 120 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Release

on:
push:
tags:
- 'v*'

# A single v<version> tag publishes all four artifacts from one commit:
# - @tyler-rng/sprite-core (npm — GitHub Packages)
# - @tyler-rng/sprite-core-client (npm — GitHub Packages)
# - ai.openclaw.spritecore:sprite-core-client(-android) (Maven — GitHub Packages)
# - SpriteCoreClient (SwiftPM consumes the git tag directly)
#
# All publish jobs depend on `verify-versions`, which fails the whole release
# if any package.json / Gradle version disagrees with the tag.

jobs:
verify-versions:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.resolve.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- id: resolve
name: Resolve tag version
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
echo "TAG_VERSION=$TAG_VERSION" >> "$GITHUB_ENV"
- name: Enforce all packages match tag
run: node scripts/check-versions.mjs "$TAG_VERSION"

publish-plugin:
needs: verify-versions
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
defaults:
run:
working-directory: packages/plugin
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://npm.pkg.github.com'
scope: '@tyler-rng'
- name: Publish plugin to GitHub Packages
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish-client-js:
needs: verify-versions
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://npm.pkg.github.com'
scope: '@tyler-rng'
- uses: pnpm/action-setup@v4
with:
version: 9
- name: pnpm install
run: pnpm install --frozen-lockfile
- name: Build schema + client-js
run: pnpm --filter ./schema --filter ./packages/client-js run --if-present build
- name: Publish client-js to GitHub Packages
working-directory: packages/client-js
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish-client-kotlin:
needs: verify-versions
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: gradle/actions/setup-gradle@v4
- name: Publish :core + :android to GitHub Packages
working-directory: packages/client-kotlin
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gradle \
:core:publishMavenPublicationToGitHubPackagesRepository \
:android:publishReleasePublicationToGitHubPackagesRepository \
-Pversion="${{ needs.verify-versions.outputs.version }}" \
--no-daemon

validate-client-swift:
needs: verify-versions
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: swift-actions/setup-swift@v2
with:
swift-version: "5.10"
- name: swift test
working-directory: packages/client-swift
run: swift test
# SwiftPM consumers pull by git tag — no publish step. This job exists
# purely to gate the tag: if swift test fails, the tag shouldn't stand.
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
node_modules/
dist/
packages/plugin/ui-dist/
.DS_Store
*.log
.env
.env.local
coverage/
.turbo/
*.tsbuildinfo

# Kotlin / Gradle
packages/client-kotlin/**/build/
packages/client-kotlin/**/.gradle/
packages/client-kotlin/.gradle/
packages/client-kotlin/local.properties

# Swift / SwiftPM
packages/client-swift/.build/
packages/client-swift/.swiftpm/
packages/client-swift/Package.resolved
Loading
Loading