NPC Library
Build your own NPC
Any script can become a coworker: a backup job, a deploy pipeline, a sensor, the coffee machine. Push its status and it walks into your office — and you can even draw the pixel-art machine it works at.
- 1
Get a key
In your office, open Connect AI coworkers and create a personal agent key (team admins can also create team keys in Settings → AI coworkers). Save it once on your machine with
npx @nocville/npc initor put it in theNOCVILLE_API_KEYenvironment variable. - 2
Say hello with one request
An NPC is created the first time something pushes a status for a new
serviceId. Try it — your NPC appears in the office straight away:Terminalcurl -X POST https://nocville.com/api/v1/status \ -H "Authorization: Bearer $NOCVILLE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"serviceId":"backup-bot","kind":"backup-bot","name":"Backup Bot", "status":"healthy","mood":"happy","message":"Last backup 2h ago"}'statusishealthy,warning,criticalorunknown;messageis its speech bubble;kindnames its type so your team can send every NPC of that type to one room. Push"offline": truewhen it shuts down and it leaves immediately. - 3
Turn it into a program
The
@nocville/npcpackage runs the loop for you: it checks on a schedule, sends one push per cycle, keeps the NPC alive between checks and says goodbye on Ctrl-C.Node.js// backup-bot.mjs — npm i @nocville/npc import { PollingNPC, parseArgs, resolveConfig } from '@nocville/npc'; class BackupBot extends PollingNPC { get name() { return 'backup-bot'; } // also its type ("kind") get defaultDisplayName() { return 'Backup Bot'; } async collect() { const hoursAgo = await hoursSinceLastBackup(); // your check const late = hoursAgo > 26; return { status: late ? 'critical' : 'healthy', message: late ? `No backup for ${hoursAgo}h!` : `Last backup ${hoursAgo}h ago`, metrics: { hoursAgo }, screenData: { layout: 'dashboard', values: [{ label: 'LAST', value: `${hoursAgo}h`, color: late ? '#ef4444' : '#22c55e' }], }, }; } } // Uses the key saved by `npx @nocville/npc setup` / `init`, or --api-key / NOCVILLE_API_KEY. const config = resolveConfig(parseArgs(process.argv).flags, { serviceId: 'backup-bot' }); await new BackupBot({ ...config, pollInterval: 60_000 }).start(); // Ctrl-C → leaves the officePrefer Python? The
nocvillepackage pushes the same status:Python# pip install nocville from nocville import NocvilleClient client = NocvilleClient(api_key="YOUR_KEY", service_id="backup-bot") try: client.push_status("healthy", message="Last backup 2h ago", name="Backup Bot", kind="backup-bot") finally: client.shutdown() # pushes offline so the NPC leaves - 4
Give it a screen
Add a
companionand sendscreenDataeach cycle to put a screen beside your NPC: adashboard-wallof values with sparklines, alog-terminal, aticker-tape, astatus-lightand more. Values can carry a colour and a trend arrow. - 5
Draw its own pixel-art device
Make your NPC a machine of its own — a server rack with blinking lights, a printer, a little robot arm. Use the
pixel-devicecompanion and send your art as the screen data:- Draw at 32 pixels per tile: a 2×3-tile device is 64×96, the same scale as the office furniture.
- Use a transparent background so it stands on the floor like real furniture, and keep to about 32 colours.
- Animate it by stacking up to 16 frames top to bottom in one PNG (1–12 fps).
- Swap the image when its state changes — green lights when healthy, flashing red when critical.
- Set
busy: 'seek-companion'and your NPC walks over and works at it. It stays there whenactivityis'busy'or'critical'. Avoidactivity: 'waiting'for degraded states: waiting freezes the NPC where it stands and raises its hand, so it never reaches its device. Send'busy'for warning instead.
Node.jsimport { PollingNPC, pixelArtFromPng, pixelDevice } from '@nocville/npc'; class BackupBot extends PollingNPC { get name() { return 'backup-bot'; } // A 2×3-tile device beside the NPC: draw it at 64×96 (32px per tile). get companion() { return pixelDevice({ w: 2, h: 3, label: 'backups' }); } // While busy, the NPC walks over and works at its device. get behavior() { return { busy: 'seek-companion' }; } async collect() { const ok = await lastBackupOk(); return { status: ok ? 'healthy' : 'critical', message: ok ? 'Backups OK' : 'Backup failed!', metrics: {}, // Two 64×96 frames stacked in one PNG → blinking LEDs at 2 fps. screenData: pixelArtFromPng(ok ? 'art/rack-ok.png' : 'art/rack-alarm.png', { frameHeight: 96, fps: 2, }), }; } } - 6
Run it and place it
Keep the program running (a terminal,
pm2, a systemd service or a container). In the office, click your NPC and choose which room it works in — tick the box to send every NPC of the samekindthere too. - 7
Share it
Built something your teammates — or other teams — would enjoy? Submit it to the NPC Library for review, or suggest an idea you'd like someone to build.
Let your AI coding tool build it
Copy this prompt into Claude Code, Codex, Cursor or any AI coding assistant. Replace the “What I want this NPC to do” section with your idea — it contains everything the AI needs to build, draw and run your NPC.
You are helping me build a custom NPC for Nocville (https://nocville.com), a pixel-art virtual office where each NPC is a character that represents a service, job or tool and shows its status. Build it as a small, long-running program that I can run on my machine or a server.
## What I want this NPC to do
<Describe it here: what it watches or does, what "healthy", "warning" and "critical" mean, how often to check, what it should show on its screen or device.>
## How Nocville NPCs work
- An NPC appears in my team's office the first time its program pushes a status, and leaves when the program pushes `offline: true` (clean shutdown).
- A push is `POST https://nocville.com/api/v1/status` with header `Authorization: Bearer <API key>` and a JSON body. Pushes are rate-limited to 60 per minute per serviceId: send ONE composed push per cycle (every 15–60 s is typical), never one per event.
- Body fields:
- `serviceId` (required, ≤100 chars): stable id; one NPC per serviceId.
- `status` (required): `healthy` | `warning` | `critical` | `unknown`.
- `mood` (required): `happy` | `concerned` | `stressed` | `idle` (usually healthy→happy, warning→concerned, critical→stressed).
- `message` (≤500 chars): one short line shown in its speech bubble and card.
- `name` (≤32 chars): banner name. `kind`: lowercase slug for the NPC's type (e.g. `backup-bot`) — teams use it to send all NPCs of a type to one room.
- `metrics`: flat object of numbers/strings shown on its card.
- `alertConfig.heartbeatInterval` (seconds, 5–3600): the NPC greys out after 2× this without a push, then leaves ~10 minutes later. Push at least that often.
- `behavior` hints: `activity` (`busy` | `idle` | `waiting` | `critical`; omit it to derive one from status/mood), `busy` (`seek-desk` | `wander` | `stay` | `seek-companion`), `idle` (`seek-lounge` | `seek-desk` | `wander` | `stay`), `idlePose` (`sit` | `sleep` | `stand`).
- What each `activity` does: `busy` goes to the `busy` target (its desk, its own device with `seek-companion`, or equipment); `critical` moves exactly like busy and adds the alert visuals; `idle` follows the `idle` hint; `waiting` FREEZES the NPC wherever it is standing and raises its hand — it does not walk anywhere. Use `waiting` only for "blocked on a human" (e.g. an agent awaiting approval), never for a degraded/warning service: send `busy` for warning so it stays at its device, and `critical` for critical.
- `companion`: an object shown beside the NPC: `{ "screenType": ..., "size": { "w": 1-6, "h": 1-6 } (tiles), "placement": "freestanding" | "adjacent-wall" | "on-desk", "label": "..." }`. Built-in screen types: `dashboard-wall`, `log-terminal`, `ticker-tape`, `crt-monitor`, `status-light`, `gauge-panel`, `commit-board`, `welcome-sign`, `web-view`, and `pixel-device` (my own pixel art).
- `screenData`: what the companion shows: `{ "layout": "...", "values": [{ "label", "value", "color"?, "trend"?: "up"|"down"|"flat", "sparkline"?: number[] }] }`.
- `links`: up to a few `{ "label", "url" }` (http/https) shown on its card.
- Use the official SDK instead of raw HTTP when possible. Node: `npm i @nocville/npc` — subclass `PollingNPC`, implement `get name()` and `async collect()` returning `{ status, mood?, message, metrics, screenData?, behavior? }`; override `get companion()`, `get behavior()`, `get archetype()` and `get defaultDisplayName()` as needed; build the config with `resolveConfig(parseArgs(process.argv).flags, { serviceId: '<id>' })` and call `.start()` (it handles Ctrl-C, the offline push and heartbeats). Python: `pip install nocville`, `NocvilleClient(api_key=..., service_id=...)`, `push_status(...)` with keyword fields (`kind=`, `behavior=`, `companion=`), and `shutdown()` in a `finally` block.
- The API key: read it from the `NOCVILLE_API_KEY` environment variable or the saved config (`npx @nocville/npc init`). Never hard-code it or print it.
## Custom pixel-art device (optional, encouraged)
- Set `companion` to `{ "screenType": "pixel-device", "size": { "w": W, "h": H }, "placement": "freestanding" }` and push the art as `screenData` with `layout: "pixel-image"`.
- Art is 32 pixels per tile (a 2×3-tile device is 64×96 px), in the style of cozy 16-bit office pixel art: dark outline, 3–4 shades per material, light from the top-left, a transparent background. Keep to ≤32 colours.
- Animate with up to 16 frames stacked vertically in one PNG (e.g. blinking LEDs), 1–12 fps. Change the art to show state (green lights when healthy, red when critical) — push a new image only when the state changes.
- In Node use `pixelArtFromPng(path, { frameHeight, fps })` and `pixelDevice({ w, h, label })` from `@nocville/npc`. Raw format, if you encode it yourself: values `fmt` = `idx8;<W>x<H>;p<colours>[;t0][;f<frames>;fps<n>]` (`t0` = palette index 0 is transparent), `palette` = base64 of RGB bytes, `pixels` = base64 of one palette index per pixel, frames top to bottom; each value ≤220,000 chars.
- Set `behavior.busy` to `seek-companion` so the NPC walks over and works at its device while busy (and critical). Don't send `activity: "waiting"` for a degraded state — that freezes it away from the device.
## Deliverables
1. The program (well-structured, typed where the language allows, with timeouts on every network call, no secrets in logs, and a clean shutdown that pushes offline).
2. If it has a device: the PNG art (or code that generates it) plus a small preview I can open.
3. A README with how to install, configure and run it (including as a background service), and what each status means.
Test it against my office with a short run before finishing.