first commit

This commit is contained in:
2026-02-25 23:49:54 -05:00
commit 4d097161cb
1775 changed files with 452827 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
# OpenRouter Model Sync Script
## Context
User wants a Python script that fetches all available models from the OpenRouter API (`GET https://openrouter.ai/api/v1/models`) and generates:
1. `~/.litellm/config.yaml` — LiteLLM proxy config with all matching models
2. `~/.env_keys.example` — single `OPENROUTER_API_KEY` entry
Filtering is controlled by a YAML config file with provider-level and model-name-pattern include/exclude rules. The script overwrites (not merges) the target files.
## Files to Create
### 1. `~/.litellm/openrouter_sync.yaml` — Filter config (with defaults/example)
```yaml
# Which providers/models to include or exclude from OpenRouter.
# Filters are evaluated in order: include first, then exclude.
# Glob patterns supported (fnmatch-style).
filters:
providers:
include: [] # empty = all providers included
exclude: [] # e.g. ["openrouter", "auto"]
models:
include: [] # empty = all models included (after provider filter)
exclude: [] # e.g. ["*:free", "*-preview", "*/old-*"]
# Output paths (defaults shown)
output:
config_yaml: "~/.litellm/config.yaml"
env_keys: "~/.env_keys.example"
```
**Filter logic:**
- `providers.include`: if non-empty, only models from these providers are kept. Provider = first path segment of model id (e.g. `google` from `google/gemini-2.0-flash`).
- `providers.exclude`: remove models from these providers (applied after include).
- `models.include`: if non-empty, only model ids matching any of these glob patterns are kept.
- `models.exclude`: remove model ids matching any of these glob patterns.
### 2. `~/.litellm/sync_openrouter.py` — Main script
**Logic:**
1. Load filter config from `~/.litellm/openrouter_sync.yaml` (create default if missing).
2. `GET https://openrouter.ai/api/v1/models` — fetch all models.
3. Apply provider include/exclude filters.
4. Apply model name include/exclude filters (fnmatch glob).
5. Build LiteLLM `model_list` entries:
```yaml
model_list:
- model_name: google/gemini-2.0-flash # the OpenRouter model id
litellm_params:
model: openrouter/google/gemini-2.0-flash
api_key: os.environ/OPENROUTER_API_KEY
```
6. Write `config.yaml` (overwrite).
7. Write `.env_keys.example` with single line: `export OPENROUTER_API_KEY="sk-or-REPLACE_ME"`.
8. Print summary: total models fetched, filtered count, written count.
**Dependencies:** `requests`, `pyyaml` (both common; script will check and error clearly if missing).
**No external packages beyond stdlib+requests+pyyaml.** Uses `fnmatch` from stdlib for glob matching.
## Verification
1. Run `python ~/.litellm/sync_openrouter.py` with default (empty) filters — should populate config.yaml with all OpenRouter models.
2. Edit `openrouter_sync.yaml` to exclude a provider, re-run, confirm those models are gone.
3. Verify `~/.env_keys.example` contains the OPENROUTER_API_KEY line.
4. Validate generated `config.yaml` with `python -c "import yaml; yaml.safe_load(open('config.yaml'))"`.

View File

@@ -0,0 +1,158 @@
# Replace Postgres Sync Bridge with Native Async pg.Pool
## Context
The app currently uses a "sync bridge" (`dbPostgresSyncBridge.js`) that wraps async Postgres queries in a worker thread and blocks the main thread with `Atomics.wait` polling at 10ms intervals. Every query pays: serialization + MessageChannel IPC + worker thread round-trip + 10ms polling tax. This makes Postgres mode ~10-100x slower than SQLite and fragile (SQLite SQL translated on the fly).
The fix: use `pg.Pool` natively with async/await. The async pool, query helpers, and full Postgres schema already exist in `dbPostgres.js`. The work is making the call chain async.
## Design
### Async DB Interface
Both SQLite and Postgres return the same async interface:
```js
{
engine: 'postgres' | 'sqlite',
prepare(sql) { async get(...p), async all(...p), async run(...p) },
async exec(sql),
async close(),
}
```
- `prepare()` is synchronous (captures SQL, returns statement object)
- `get/all/run/exec` are async (return Promises)
- For SQLite: the async methods resolve synchronously (zero overhead)
### Transaction Isolation via AsyncLocalStorage
**Problem**: With `pg.Pool`, concurrent requests get different connections — a `BEGIN` on one connection doesn't affect queries on another.
**Solution**: Module-scoped `AsyncLocalStorage` in `dbPostgres.js`. When `tx()` starts a transaction, it checks out a client from the pool, pins it in `AsyncLocalStorage`, and all `db.prepare()` calls within that async context automatically route to the pinned client. The callback uses the same `db` variable from the closure — no parameter changes needed.
```js
// Postgres tx
const client = await db.pool.connect();
await client.query('BEGIN');
const result = await txStorage.run({ client }, fn); // fn's db calls see pinned client
await client.query('COMMIT');
client.release();
```
Nested `tx()` calls detect the existing store and run inline (no double-BEGIN).
### `openDatabase()` becomes async
- **Postgres**: `createPostgresPool()``initializePostgresSchema(pool)``createAsyncPostgresDb(pool)` → return
- **SQLite**: `new DatabaseSync()` → sync `initializeSchema()``wrapSyncDbAsAsync()` → return
Schema init stays engine-specific: sync for SQLite (unchanged), async for Postgres (already exists).
### Return value contracts (unchanged)
- `.get()` → row or `undefined`
- `.all()` → array (empty if no results)
- `.run()``{ changes: number, lastInsertRowid: null }`
## Implementation Steps
### Step 1: `src/dbPostgres.js` — Add async db adapter
- Add `AsyncLocalStorage` import and module-scoped `txStorage`
- Add `createAsyncPostgresDb(pool)` — returns async db object with `getClient()` routing via `txStorage`
- Add `export async function asyncTx(db, fn)` — acquires client, pins in ALS, runs fn, commits/rollbacks
- Keep all existing exports (pool creation, schema init, param conversion)
### Step 2: `src/db.js` — Add SQLite async wrapper + async openDatabase
- Add `wrapSyncDbAsAsync(syncDb)` — wraps sync prepare/exec/close in async functions
- Make `openDatabase()` async:
- Postgres path: pool + async schema init + `createAsyncPostgresDb(pool)`
- SQLite path: sync init + `wrapSyncDbAsAsync(syncDb)`
- Remove `openPostgresSyncBridge` import
- Keep sync `initializeSchema()` (only used for SQLite, called on raw `DatabaseSync`)
### Step 3: `src/store/coreStore.js` — Convert store layer to async
The bulk of the work (~200 lines touched, all mechanical):
- Make `tx()` async and driver-aware (calls `asyncTx` for Postgres, inline BEGIN/COMMIT for SQLite)
- 82 exported functions → `async function`
- ~15 internal helpers → `async function`
- 85 `db.prepare().get/all/run()` calls → `await`
- 12 `tx(db, () => ...)` calls → `await tx(db, async () => ...)`
- Store-to-store calls (e.g. `listRoster` inside `getGroupSummary`) → `await`
### Step 4: `src/itemCatalog.js` — Convert to async
- 7 functions become async
- 9 db calls get `await`
- Manual `BEGIN`/`COMMIT` in `refreshItemCatalog` and `rollbackItemCatalogToSnapshot` → use `tx()`
### Step 5: All 7 route files — Make handlers async
80 route handlers across:
- `src/routes/syncRoutes.js` (14)
- `src/routes/groupRoutes.js` (27)
- `src/routes/authRoutes.js` (13)
- `src/routes/webRoutes.js` (10)
- `src/routes/billingRoutes.js` (7 + 2 direct db calls)
- `src/routes/opsRoutes.js` (6)
- `src/routes/internalRoutes.js` (3)
Each: `(req, res) =>``async (req, res) =>`, add `await` before store calls. Express 5 handles async errors natively.
### Step 6: `src/server.js` — Async startup + local functions
- `const db = await openDatabase()` (top-level await in ESM)
- ~12 local functions that call db become async: `probeDatabaseHealth`, `wipeRuntimeDataForDev`, `findGroupByJoinCode`, `lookupGroupAccessByUser`, `readAuthenticatedWebUser`, `requireAuthenticatedWebUser`, `requireGroupMembership`, `getRequestPremiumState`, `getRequestFeatureState`, `isApiFeatureEnabled`, `requireApiFeatureFlag`, `publishDashboardEventForUserGroups`, etc.
- `await seedDemoData(db)` at startup
- Add pool shutdown on SIGINT/SIGTERM
### Step 7: `src/activityWebhookDelivery.js`
- Already async, just add `await` to 2 store function calls
### Step 8: Delete sync bridge
- Delete `src/dbPostgresSyncBridge.js`
- Delete `src/dbPostgresSyncWorker.js`
- Update `resolveDatabaseRuntimeConfig()` to remove `sync_bridge` compatibility reference
### Step 9: Update tests
- 32 test files: test callbacks become `async`, db created via `wrapSyncDbAsAsync()`, store calls get `await`
## Files Modified
| File | Change |
|------|--------|
| `src/dbPostgres.js` | Add `createAsyncPostgresDb`, `asyncTx`, `txStorage` (~60 new lines) |
| `src/db.js` | Add `wrapSyncDbAsAsync`; async `openDatabase`; remove sync bridge import |
| `src/store/coreStore.js` | All 82 exports + ~15 helpers async; await 85 prepare + 12 tx calls |
| `src/itemCatalog.js` | 7 functions async; await 9 db calls; replace manual tx |
| `src/routes/*.js` (7 files) | 80 handlers async; await store calls |
| `src/server.js` | Async startup; ~12 local functions async; pool shutdown |
| `src/activityWebhookDelivery.js` | Await 2 store calls |
## Files Deleted
- `src/dbPostgresSyncBridge.js`
- `src/dbPostgresSyncWorker.js`
## Key Risks
1. **SQLite transaction interleaving**: Won't happen — SQLite async wrapper resolves synchronously (no real yielding between BEGIN/COMMIT). Rule: no network I/O inside `tx()` callbacks.
2. **Pool exhaustion**: `asyncTx` uses `try/finally` to guarantee `client.release()`. Pool timeout (10s default) prevents silent hangs.
3. **Return value `.get()` undefined vs null**: New adapter returns `undefined` (matching SQLite behavior). All call sites use `if (!row)` — works for both.
4. **Cascading async in server.js**: `getRequestPremiumState``getRequestFeatureState``isApiFeatureEnabled` etc. chain needs async. All call sites are in route handlers (already being made async).
## Verification
1. `npm test` — all 32 test files pass with async wrapper
2. `docker compose down -v && docker compose up --build -d` — app starts cleanly
3. Full onboarding flow: mock OAuth → create group → plugin sync → vault shows items
4. Check Docker logs: no `Atomics`, no worker thread, no 10ms polling
5. Compare response times: `time curl http://localhost:3000/api/v1/groups/:id/vault` (before/after)
6. Verify SQLite mode still works: `DB_DRIVER=sqlite npm start`

View File

@@ -0,0 +1,219 @@
# Cattopia: Hero, Parrot, Pirate Ship & Level 2
## Context
The refactoring to PixiJS/GSAP/Vite/ES6 modules is complete. Now we're adding new gameplay: a player-controlled Hero (auto-move + click-to-attack), a pet Parrot companion, both fighting alongside cats in all levels. After defeating the Blood Wolf (boss 5), a pirate ship transition leads to Level 2 where skeleton rats replace mice and the final boss — Lord of the Rat Dead — summons skeleton rats.
## Game Flow (Updated)
```
Intro → Creator (cats + hero + parrot) → Level 1 (hunt mice → 5 bosses)
→ Pirate Ship Transition → Level 2 (hunt skeleton rats → Lord of the Rat Dead)
→ Victory
```
## New Files
```
src/
entities/
Hero.js (auto-move AI + click-attack, stats, XP/leveling)
Parrot.js (follows hero, auto-attacks, stats)
SkeletonRat.js (Level 2 prey — tougher than Mouse, bone visual)
renderers/
HeroRenderer.js (dark green hoodie, emerald knife, legs, attack anim)
ParrotRenderer.js (colorful bird body, flapping wings, color-customizable)
SkeletonRatRenderer.js (bone-colored rat with skeletal details)
PirateShipRenderer.js (ship hull, mast, sails, water — used by transition scene)
scenes/
PirateShipScene.js (boarding animation: cats/hero/parrot walk onto ship, ship sails off, GSAP transition)
```
## Modified Files
| File | Changes |
|---|---|
| `src/constants.js` | Add `LEVEL_2_BOSS_DEF`, `SKELETON_RAT_GOAL`, new game phases (`SHIP_TRANSITION`) |
| `src/state/GameState.js` | Add `hero`, `parrot`, `skeletonRats[]`, `currentLevel` (1 or 2) |
| `src/scenes/CreatorScene.js` | Add hero creator section (name + color) and parrot creator section (name + color) |
| `src/scenes/GameScene.js` | Integrate hero + parrot into update/render loops, handle click-to-attack, Level 2 skeleton rat logic, Lord of the Rat Dead boss |
| `src/scenes/VictoryScene.js` | Include hero + parrot in final scoreboard |
| `src/main.js` | Register new PirateShipScene |
| `index.html` | Add hero + parrot creator DOM elements |
| `styles/style.css` | Styles for hero/parrot creator sections |
---
## Phase 1: Constants, State & Entity Classes
### 1a. Update `src/constants.js`
- Add `SHIP_TRANSITION` to `GAME_PHASES`
- Add Level 2 constants:
```js
export const SKELETON_RAT_GOAL = 25;
export const LEVEL_2_BOSS_DEF = {
name: 'Lord of the Rat Dead',
icon: '💀🐀',
color: '#2a1a2a',
loot: { name: "Necro Crown", icon: '💀👑', stats: { hunting: 12, speed: 8, agility: 8, stealth: 8 } },
summonInterval: 300, // frames between skeleton rat summons
hpPerAlly: 250 // HP per cat+hero
};
```
### 1b. Update `src/state/GameState.js`
- Add to `reset()`:
```js
this.hero = null;
this.parrot = null;
this.skeletonRats = [];
this.currentLevel = 1;
```
### 1c. Create `src/entities/Hero.js`
- Similar structure to `Cat.js` — has name, color, stats, level, xp, gear
- `updateHunt(targets, bounds)`: auto-move AI chases nearest mouse/skeleton rat (same pattern as Cat.updateHunt)
- `updateBossChase(boss)`: auto-move toward boss (same pattern as Cat.updateBossChase)
- `triggerAttack(enemies, boss)`: called on player click — finds nearest enemy within ~80px range and deals bonus damage burst. Returns `{ type: 'attackHit', target, damage }` or null
- `applyPhysics(bounds)`: identical to Cat
- Distinguishing stats: higher base hunting, lower stealth (knife fighter)
### 1d. Create `src/entities/Parrot.js`
- Has name, color, stats (auto-generated like cats), level, xp
- `update(hero, targets, boss, bounds)`: follows hero loosely (offset position), periodically attacks nearest enemy within range (every ~90 frames). Returns action: `{ type: 'peck', target, damage }` or null
- `applyPhysics(bounds)`: same clamping pattern
- Smaller hitbox, faster movement, lower damage than hero
### 1e. Create `src/entities/SkeletonRat.js`
- Based on `Mouse.js` but tougher: `hp` field (takes 2-3 hits to kill), slightly faster
- `update(hunters, bounds)`: flee AI like Mouse but with more erratic movement
- `alive` flag set to false when hp <= 0
## Phase 2: Renderers
### 2a. Create `src/renderers/HeroRenderer.js`
- Follow `CatRenderer.js` pooling pattern (single hero, but consistent API)
- Draw: human-ish figure (slightly taller than cats), dark green hoodie body, hood, legs (animated walk cycle), arms
- Emerald knife: green-tinted blade held in right hand, glows on attack
- Attack animation: knife slash arc (brief GSAP or frame-based flash)
- Name tag above head like cats
### 2b. Create `src/renderers/ParrotRenderer.js`
- Colorful bird body using user-selected color as base
- Flapping wings (oscillating wing angle per frame)
- Small beak, tail feathers, beady eyes
- Name tag (smaller than hero/cat)
### 2c. Create `src/renderers/SkeletonRatRenderer.js`
- Based on `MouseRenderer.js` but bone-white/gray color palette
- Visible ribs/bones on body, hollow eye sockets
- HP bar above (since they take multiple hits)
- Scared state: bones rattle (slight shake)
### 2d. Create `src/renderers/PirateShipRenderer.js`
- Ship hull (brown, curved), mast with sails, jolly roger flag
- Water beneath (blue waves, animated)
- Used by PirateShipScene for the transition
## Phase 3: Creator Scene Updates
### 3a. Update `index.html`
- Inside `#creator-screen > .creator-panel`, add after cat creation form:
- Hero section: name input + color picker (dark green hoodie preview + alternate colors)
- Parrot section: name input + color picker (red/blue/green/yellow parrot previews)
- Reorder: cats first, then hero, then parrot, then "Start the Hunt!" button
### 3b. Update `src/scenes/CreatorScene.js`
- Add hero creation logic: name + color → `new Hero({...})` → `gameState.hero = hero`
- Add parrot creation logic: name + color → `new Parrot({...})` → `gameState.parrot = parrot`
- Stats reveal for hero and parrot (same animated bar pattern as cats)
- Require at least 1 cat + hero + parrot before "Start the Hunt!" enables
- Add hero/parrot to roster display
### 3c. Update `styles/style.css`
- Styles for hero/parrot creator sections, color picker buttons, preview icons
## Phase 4: GameScene Integration (Level 1 + Level 2)
### 4a. Add hero to update loop (`GameScene.js`)
- Hunt phase: `hero.updateHunt(currentTargets, bounds)` where `currentTargets` = mice (L1) or skeletonRats (L2)
- Boss phase: `hero.updateBossChase(boss)`
- Catch/damage checks same pattern as cats but with hero's stats
- Click handler on PixiJS container: `this.container.on('pointerdown', (e) => this._onHeroAttack(e))`
- `_onHeroAttack(e)`: call `hero.triggerAttack()` to find and damage nearest enemy
### 4b. Add parrot to update loop
- Every frame: `parrot.update(hero, targets, boss, bounds)`
- Process returned peck actions: apply damage, spawn hit particles, award XP to parrot
### 4c. Add renderers to GameScene
- Create `HeroRenderer`, `ParrotRenderer`, `SkeletonRatRenderer` instances
- Add containers in render order (hero between cats and giga dog, parrot near hero)
- Sync in `render()`: `this.heroRenderer.sync(gameState.hero)`, `this.parrotRenderer.sync(gameState.parrot)`, `this.skeletonRatRenderer.sync(gameState.skeletonRats)`
### 4d. Level 2 hunt logic
- When `gameState.currentLevel === 2`:
- "Add Mouse" buttons become "Add Skeleton Rat" buttons
- Mice array unused; skeleton rats array used instead
- Catch = skeleton rat HP reduced; when HP <= 0, award XP + increment totalCaught
- Goal: `SKELETON_RAT_GOAL` skeleton rats killed triggers Lord of the Rat Dead
### 4e. Lord of the Rat Dead boss
- Reuse `Boss.js` entity with a new defIndex (6) or create specialized subclass
- **Approach**: extend `BOSS_DEFS` array with the Lord entry at index 5 (only used in Level 2)
- Boss mechanics: standard movement + periodically summons skeleton rats (using `summonInterval` from def)
- On defeat: transition to victory (`sceneManager.switchTo('victory')`)
### 4f. Update `_defeatBoss()` flow
- After Blood Wolf defeat (boss index 4, Level 1): instead of victory, set `gameState.currentLevel = 2` and `sceneManager.switchTo('ship')`
- After Lord of the Rat Dead defeat (Level 2): go to victory
## Phase 5: Pirate Ship Transition Scene
### 5a. Create `src/scenes/PirateShipScene.js`
- PixiJS scene with PirateShipRenderer for the ship + water background
- GSAP timeline:
1. Show "The seas await..." text overlay (fade in)
2. Cats, hero, and parrot walk/fly from left toward ship (small sprites moving right)
3. Board the ship (sprites move onto deck)
4. Ship sails right, screen fades to black
5. Switch to GameScene with Level 2 state
- Duration: ~4-5 seconds total
### 5b. Update `src/main.js`
- Import and register PirateShipScene: `sceneManager.register('ship', pirateShipScene)`
### 5c. Update GameScene.enter()
- Check `gameState.currentLevel`:
- Level 1: current behavior (forest, mice)
- Level 2: reset totalCaught, update HUD text ("Skeleton Rats" instead of "Mice"), different forest theme (darker/undead aesthetic via ForestRenderer flag), spawn skeleton rats
### 5d. Optional: ForestRenderer Level 2 theme
- When `currentLevel === 2`: darker sky gradient, dead/gray trees, eerie green ground glow
- Reuse blood moon infrastructure but with green/purple tint instead of red
## Phase 6: Victory Scene Updates
### 6a. Update `VictoryScene.js`
- Include hero and parrot in final scoreboard alongside cats
- Hero gets a special "Hero" badge, parrot gets "Companion" badge
- MVP calculation includes hero (hero.catches or boss damage dealt)
## Phase 7: Polish & Cleanup
- Update SpawnSystem with `spawnSkeletonRats()` method
- Update `_updateHUD()` to show level indicator ("Level 1" / "Level 2")
- Update `_updateScoreboard()` to include hero + parrot entries
- Ensure Giga Dog works in both levels
- Update README with new features
---
## Verification
After each phase, verify with `npm run dev`:
1. **Creator**: Can create cats, hero (name + color), and parrot (name + color). All show in roster with stats.
2. **Level 1**: Hero auto-moves and chases mice. Click near hero attacks. Parrot follows hero and pecks enemies. All 5 bosses work.
3. **Ship Transition**: After Blood Wolf defeat, pirate ship scene plays, transitions to Level 2.
4. **Level 2**: Skeleton rats spawn instead of mice. Hunt → Lord of the Rat Dead boss. Boss summons skeleton rats. Defeating boss triggers victory.
5. **Victory**: Scoreboard shows cats, hero, and parrot. MVP includes all.
6. **Full build**: `npm run build` succeeds, Docker still works.