@caperjs/core 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,13 @@ It sits on top of PixiJS — a renderer, not a game engine — and adds the part
8
8
 
9
9
  ## Status
10
10
 
11
- This package is **not yet published to npm**. The recommended way to use it is to clone the [Caper monorepo](https://github.com/anthonysapp/caper), run the kitchen-sink, and either fork it as a starter or vendor `packages/core` into your own project. First publish lands in Phase 7.
11
+ Published on npm as `@caperjs/core`.
12
+
13
+ ## For AI agents
14
+
15
+ - `npx caper agent init` installs the `caper` skill + `AGENTS.md` pointers for this project.
16
+ - The reference is `node_modules/@caperjs/core/extras/llms.txt` — read it by section (`grep -n '^## '`), not whole.
17
+ - Engine source ships in the package under `src/`.
12
18
 
13
19
  ## Highlights
14
20
 
@@ -69,29 +69,39 @@ async function generateAssetTypes(manifest, assetsDir) {
69
69
  addToBundle('spritesheets', bundle.name, alias);
70
70
  });
71
71
 
72
- // Extract frame names from TPS JSON files
72
+ // Extract frame names from TPS JSON files, following the multipack
73
+ // chain. A sheet too big for one page is split, and only page 0 reaches
74
+ // the manifest — the rest hang off its `meta.related_multi_packs`, which
75
+ // is how PixiJS finds them too. Reading page 0 alone would type only the
76
+ // frames the packer happened to fit there, so a frame's alias would
77
+ // appear and vanish as unrelated art shifted the packing.
73
78
  if (asset.data?.tags?.tps) {
74
- try {
75
- // Find the first .json file in the src array
76
- const jsonSrc = srcs.find((src) => src.endsWith('.json'));
77
- if (jsonSrc) {
78
- // Construct the full path to the JSON file
79
+ const queue = srcs.filter((src) => src.endsWith('.json')).slice(0, 1);
80
+ const seen = new Set(queue);
81
+ while (queue.length) {
82
+ const jsonSrc = queue.shift();
83
+ try {
79
84
  const jsonPath = path.join(assetsDir, jsonSrc);
85
+ const tpsData = JSON.parse(await fs.promises.readFile(jsonPath, 'utf8'));
80
86
 
81
- // Read and parse the JSON file
82
- const jsonContent = await fs.promises.readFile(jsonPath, 'utf8');
83
- const tpsData = JSON.parse(jsonContent);
87
+ for (const frameName of Object.keys(tpsData.frames || {})) {
88
+ assetsByType.tpsFrames.add(frameName);
89
+ addToBundle('tpsFrames', bundle.name, frameName);
90
+ }
84
91
 
85
- // Extract frame names from the "frames" object
86
- if (tpsData.frames) {
87
- Object.keys(tpsData.frames).forEach((frameName) => {
88
- assetsByType.tpsFrames.add(frameName);
89
- addToBundle('tpsFrames', bundle.name, frameName);
90
- });
92
+ // Related pages are named relative to the page that lists them.
93
+ // `seen` keeps a self- or cross-referencing chain from looping.
94
+ const dir = path.dirname(jsonSrc);
95
+ for (const related of tpsData.meta?.related_multi_packs || []) {
96
+ const next = path.join(dir, related);
97
+ if (!seen.has(next)) {
98
+ seen.add(next);
99
+ queue.push(next);
100
+ }
91
101
  }
102
+ } catch (error) {
103
+ logger.warn(`Failed to load TPS frames from ${jsonSrc}:`, error.message);
92
104
  }
93
- } catch (error) {
94
- logger.warn(`Failed to load TPS frames from ${firstSrc}:`, error.message);
95
105
  }
96
106
  }
97
107
  } else if (ext === '.json' && !firstSrc.includes('atlas')) {
package/cli/agent.mjs ADDED
@@ -0,0 +1,106 @@
1
+ import { bgRed, bold, cyan, green, white } from 'kleur/colors';
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ /**
8
+ * `caper agent init` — installs the shipped `caper` agent skill into the
9
+ * current app and upserts a marker-delimited pointer block into the app's
10
+ * agent context file (`AGENTS.md` or `CLAUDE.md`).
11
+ */
12
+
13
+ const START_MARKER = '<!-- caper:agent-start -->';
14
+ const END_MARKER = '<!-- caper:agent-end -->';
15
+
16
+ // `.href` so this also works where the global URL is a DOM polyfill (vitest + happy-dom).
17
+ function resolveModuleRelative(rel) {
18
+ return fileURLToPath(new URL(rel, import.meta.url).href);
19
+ }
20
+
21
+ function readVersion() {
22
+ return JSON.parse(fs.readFileSync(resolveModuleRelative('../package.json'), 'utf-8')).version;
23
+ }
24
+
25
+ function makeBlock(skillPath, version) {
26
+ return `${START_MARKER}
27
+ ## Caper (engine) — agent pointers
28
+
29
+ This app runs on the Caper engine (\`@caperjs/core@${version}\`). Generated by \`caper agent init\`; re-run it after upgrading caper. Do not edit between these markers.
30
+
31
+ - Before engine-facing work (scenes, entities, popups, UI, plugins, \`caper.config.ts\`, assets, generated types), load the \`caper\` skill at \`${skillPath}\`.
32
+ - Read the reference by section, not whole: \`node_modules/@caperjs/core/extras/llms.txt\` (\`grep -n '^## '\` for the outline). Engine source ships at \`node_modules/@caperjs/core/src/\`.
33
+ - Verify: typecheck → build → drive the running app through \`window.Caper\` / \`Caper.automation[appId]\` (llms.txt §17). Never leave a dev server running in the foreground.
34
+ ${END_MARKER}`;
35
+ }
36
+
37
+ function upsertContext(cwd, dir) {
38
+ const candidates = ['AGENTS.md', 'CLAUDE.md'];
39
+ let contextFile = candidates.find((name) => fs.existsSync(path.join(cwd, name)));
40
+
41
+ if (!contextFile) {
42
+ contextFile = 'AGENTS.md';
43
+ }
44
+
45
+ const contextPath = path.resolve(cwd, contextFile);
46
+ const existing = fs.existsSync(contextPath) ? fs.readFileSync(contextPath, 'utf-8') : '';
47
+ const skillPath = path.posix.join(dir, 'caper/SKILL.md');
48
+ const version = readVersion();
49
+ const block = makeBlock(skillPath, version);
50
+
51
+ const startIndex = existing.indexOf(START_MARKER);
52
+ const endIndex = existing.indexOf(END_MARKER);
53
+
54
+ let contents;
55
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
56
+ const before = existing.slice(0, startIndex);
57
+ const after = existing.slice(endIndex + END_MARKER.length);
58
+ const trimmedBefore = before.replace(/\s+$/, '');
59
+ const trimmedAfter = after.replace(/^\s+/, '');
60
+ const spacerBefore = trimmedBefore ? '\n\n' : '';
61
+ const spacerAfter = trimmedAfter ? '\n\n' : '';
62
+ contents = `${trimmedBefore}${spacerBefore}${block}${spacerAfter}${trimmedAfter}`;
63
+ } else if (existing) {
64
+ const spacer = existing.endsWith('\n\n') ? '' : '\n\n';
65
+ contents = `${existing.replace(/\s+$/, '')}${spacer}${block}\n`;
66
+ } else {
67
+ contents = `${block}\n`;
68
+ }
69
+
70
+ fs.writeFileSync(contextPath, contents, 'utf-8');
71
+ return contextPath;
72
+ }
73
+
74
+ export async function agentInit(cwd, { dir = '.claude/skills' } = {}) {
75
+ const skillSource = resolveModuleRelative('../extras/skills/caper/SKILL.md');
76
+ const skillDir = path.resolve(cwd, dir, 'caper');
77
+ const skillFile = path.join(skillDir, 'SKILL.md');
78
+
79
+ fs.mkdirSync(skillDir, { recursive: true });
80
+ fs.copyFileSync(skillSource, skillFile);
81
+
82
+ const contextFile = upsertContext(cwd, dir);
83
+
84
+ console.log(green(bold('✓ Created caper skill')) + ` ${cyan(path.relative(cwd, skillFile).replace(/\\/g, '/'))}`);
85
+ console.log(green(bold('✓ Updated agent context')) + ` ${cyan(path.relative(cwd, contextFile).replace(/\\/g, '/'))}`);
86
+
87
+ return { skillFile, contextFile };
88
+ }
89
+
90
+ export async function agent(args) {
91
+ if (args[0] === 'init') {
92
+ let dir = '.claude/skills';
93
+ for (let i = 1; i < args.length; i++) {
94
+ if (args[i] === '--dir' && args[i + 1]) {
95
+ dir = args[i + 1];
96
+ i++;
97
+ }
98
+ }
99
+ await agentInit(process.cwd(), { dir });
100
+ return;
101
+ }
102
+
103
+ const subcommand = args[0] ?? '(none)';
104
+ console.error(bold(bgRed(white(`Unknown agent command: "${subcommand}". Please use "init".`))));
105
+ process.exit(1);
106
+ }
@@ -0,0 +1,104 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { agentInit } from './agent.mjs';
6
+
7
+ const shippedSkill = path.resolve(process.cwd(), 'extras/skills/caper/SKILL.md');
8
+
9
+ const START_MARKER = '<!-- caper:agent-start -->';
10
+
11
+ let tempDir = null;
12
+
13
+ afterEach(() => {
14
+ if (tempDir && fs.existsSync(tempDir)) {
15
+ fs.rmSync(tempDir, { recursive: true, force: true });
16
+ }
17
+ tempDir = null;
18
+ });
19
+
20
+ function makeTempDir() {
21
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'caper-agent-'));
22
+ return tempDir;
23
+ }
24
+
25
+ function countMarkers(contents) {
26
+ return {
27
+ start: (contents.match(/<!-- caper:agent-start -->/g) || []).length,
28
+ end: (contents.match(/<!-- caper:agent-end -->/g) || []).length,
29
+ };
30
+ }
31
+
32
+ describe('agentInit', () => {
33
+ it('creates the default skill copy and AGENTS.md when no context file exists', async () => {
34
+ const cwd = makeTempDir();
35
+
36
+ const { skillFile, contextFile } = await agentInit(cwd);
37
+
38
+ expect(fs.existsSync(skillFile)).toBe(true);
39
+ expect(fs.readFileSync(skillFile, 'utf-8')).toBe(fs.readFileSync(shippedSkill, 'utf-8'));
40
+ expect(path.basename(contextFile)).toBe('AGENTS.md');
41
+
42
+ const context = fs.readFileSync(contextFile, 'utf-8');
43
+ expect(context).toContain('<!-- caper:agent-start -->');
44
+ expect(context).toContain('<!-- caper:agent-end -->');
45
+ expect(context).toContain('.claude/skills/caper/SKILL.md');
46
+ });
47
+
48
+ it('respects --dir and references the custom skill path', async () => {
49
+ const cwd = makeTempDir();
50
+
51
+ const { skillFile, contextFile } = await agentInit(cwd, { dir: 'skills' });
52
+
53
+ expect(skillFile).toBe(path.join(cwd, 'skills/caper/SKILL.md'));
54
+ expect(fs.existsSync(skillFile)).toBe(true);
55
+
56
+ const context = fs.readFileSync(contextFile, 'utf-8');
57
+ expect(context).toContain('skills/caper/SKILL.md');
58
+ expect(context).not.toContain('.claude/skills/caper/SKILL.md');
59
+ });
60
+
61
+ it('appends the block to CLAUDE.md when only CLAUDE.md exists', async () => {
62
+ const cwd = makeTempDir();
63
+ const original = '# Project CLAUDE.md\n\nSome existing guidance.\n';
64
+ fs.writeFileSync(path.join(cwd, 'CLAUDE.md'), original, 'utf-8');
65
+
66
+ const { skillFile, contextFile } = await agentInit(cwd);
67
+
68
+ expect(path.basename(contextFile)).toBe('CLAUDE.md');
69
+ expect(fs.existsSync(path.join(cwd, 'AGENTS.md'))).toBe(false);
70
+
71
+ const context = fs.readFileSync(contextFile, 'utf-8');
72
+ expect(context).toContain('Some existing guidance.');
73
+ expect(context).toContain('<!-- caper:agent-start -->');
74
+ expect(context).toContain('<!-- caper:agent-end -->');
75
+ expect(context.indexOf('Some existing guidance.')).toBeLessThan(context.indexOf(START_MARKER));
76
+ });
77
+
78
+ it('is idempotent and preserves user edits outside the markers', async () => {
79
+ const cwd = makeTempDir();
80
+
81
+ await agentInit(cwd);
82
+ const first = fs.readFileSync(path.join(cwd, 'AGENTS.md'), 'utf-8');
83
+
84
+ // User edits outside the markers
85
+ const edited = `# App pointers\n\n${first}\n\n## Custom section\n\nKeep me.\n`;
86
+ fs.writeFileSync(path.join(cwd, 'AGENTS.md'), edited, 'utf-8');
87
+
88
+ await agentInit(cwd);
89
+ const second = fs.readFileSync(path.join(cwd, 'AGENTS.md'), 'utf-8');
90
+
91
+ const markers = countMarkers(second);
92
+ expect(markers.start).toBe(1);
93
+ expect(markers.end).toBe(1);
94
+ expect(second).toContain('# App pointers');
95
+ expect(second).toContain('## Custom section');
96
+ expect(second).toContain('Keep me.');
97
+
98
+ // Replace the block with the freshly generated one so we can compare the rest
99
+ const start = second.indexOf('<!-- caper:agent-start -->');
100
+ const end = second.indexOf('<!-- caper:agent-end -->') + '<!-- caper:agent-end -->'.length;
101
+ const regenerated = second.slice(start, end);
102
+ expect(regenerated).toBe(first.trimEnd());
103
+ });
104
+ });
package/cli/create.mjs CHANGED
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import process from 'node:process';
7
7
  import { promisify } from 'node:util';
8
8
  import shell from 'shelljs';
9
+ import { agentInit } from './agent.mjs';
9
10
  import { copy, dist, mkdirp, package_manager } from './utils.mjs';
10
11
 
11
12
  let packageManager = package_manager;
@@ -150,7 +151,7 @@ export function injectPluginsIntoConfig(configContents, pluginConfigs) {
150
151
  * @param {string} cwd
151
152
  * @param {string[]} plugins
152
153
  */
153
- function write_template_files(cwd, template, applicationNameForPkg, applicationName, defaultName, plugins) {
154
+ async function write_template_files(cwd, template, applicationNameForPkg, applicationName, defaultName, plugins) {
154
155
  const dir = dist(`templates/${template}`);
155
156
  copy(`${dir}/package.template.json`, `${cwd}/package.json`);
156
157
 
@@ -209,6 +210,14 @@ function write_template_files(cwd, template, applicationNameForPkg, applicationN
209
210
  readme_contents = readme_contents.replace(/~PACKAGE_MANAGER~/g, mgr);
210
211
  fs.writeFileSync(readme_file, readme_contents, 'utf-8');
211
212
 
213
+ // Install the shipped caper agent skill and upsert pointers into AGENTS.md/CLAUDE.md.
214
+ // This is best-effort: scaffolding must not fail because the agent init step failed.
215
+ try {
216
+ await agentInit(cwd);
217
+ } catch (e) {
218
+ console.warn('Warning: failed to install the caper agent skill:', e.message);
219
+ }
220
+
212
221
  // find __APPLICATION_NAME__.ts and replace it with the application name, then delete ~Application.ts
213
222
  const app_file = `${cwd}/src/__APPLICATION_NAME__.ts`;
214
223
  const app_contents = fs.readFileSync(app_file, 'utf-8');
package/cli.mjs CHANGED
@@ -5,6 +5,7 @@ import { bgRed, bold, green, red, white } from 'kleur/colors';
5
5
  import fs from 'node:fs';
6
6
  import process from 'node:process';
7
7
  import { add } from './cli/add.mjs';
8
+ import { agent } from './cli/agent.mjs';
8
9
  import { generateCaptions } from './cli/audio/cc.mjs';
9
10
  import { compress } from './cli/audio/index.mjs';
10
11
  import { create } from './cli/create.mjs';
@@ -64,6 +65,9 @@ switch (args[0]) {
64
65
  case 'add':
65
66
  await add(args.slice(1));
66
67
  break;
68
+ case 'agent':
69
+ await agent(args.slice(1));
70
+ break;
67
71
  case 'create': {
68
72
  let packageManager = 'npm'; // Default to npm
69
73
  let projectPath = '.'; // Default to current directory
package/extras/llms.txt CHANGED
@@ -7,7 +7,7 @@ on every `Container`, build-time auto-discovery of scenes/plugins/popups/entitie
7
7
  and generated TypeScript types for app IDs and assets. Caper is a personal fork
8
8
  of `dill-pixel` by Relish Studios.
9
9
 
10
- **Package version:** `@caperjs/core@6.2.2` · **Peer deps:** `pixi.js@8.x`, `@pixi/sound@^6`, `gsap@^3.13`.
10
+ **Package version:** see `../package.json` (this file ships inside the package) · **Peer deps:** `pixi.js@8.x`, `@pixi/sound@^6`, `gsap@^3.13`.
11
11
 
12
12
  **File citations in this guide** point at the `@caperjs/core` package source
13
13
  (paths relative to this file, e.g. `../src/core/Application.ts`) — those ship
@@ -39,7 +39,8 @@ produce working code.
39
39
  14. [Generated types & virtual modules](#14-generated-types--virtual-modules)
40
40
  15. [Common recipes](#15-common-recipes)
41
41
  16. [Gotchas & verification loop](#16-gotchas--verification-loop)
42
- 17. [API reference index](#17-api-reference-index)
42
+ 17. [Automation bridge — drive the running app](#17-automation-bridge--drive-the-running-app)
43
+ 18. [API reference index](#18-api-reference-index)
43
44
 
44
45
  ---
45
46
 
@@ -1543,7 +1544,87 @@ cache produces confusing texture/name mismatches.
1543
1544
 
1544
1545
  ---
1545
1546
 
1546
- ## 17. API reference index
1547
+ ## 17. Automation bridge — drive the running app
1548
+
1549
+ Every app registers itself on `window.Caper` at boot (source:
1550
+ [`../src/core/globals.ts`](../src/core/globals.ts)). This is how an agent,
1551
+ Playwright script, or smoke test drives a Caper app without clicking around.
1552
+
1553
+ ### 17.1 What is always there
1554
+
1555
+ ```ts
1556
+ Caper.apps // Map<appId, app> (appId = caper.config.ts `id`, else 'CaperApplication')
1557
+ Caper.app // last created app
1558
+ await Caper.ready() // resolves when the first app has fully booted (safe to call early)
1559
+ await Caper.ready('my-app-id')
1560
+ ```
1561
+
1562
+ ### 17.2 The automation facade (gated)
1563
+
1564
+ Enabled when **any** of: dev server (`import.meta.env.DEV`), `automation: true`
1565
+ in `caper.config.ts`, or env `VITE_CAPER_AUTOMATION=true` at build time. When on,
1566
+ `Caper.automation[appId]` (also `app.automation`) exposes:
1567
+
1568
+ | Member | What it does |
1569
+ | --- | --- |
1570
+ | `action(name, data?)` | `app.sendAction(name, data)`. Subject to the ActionsPlugin context rules — out-of-context actions are dropped, same as a real input. |
1571
+ | `getContext()` | Current action context as a string (e.g. `'game'`, `'popup'`). |
1572
+ | `getState()` | Whatever the app's registered state getter returns (`undefined` until registered). |
1573
+ | `registerStateGetter(fn)` | App code calls this once to expose a snapshot of game state. |
1574
+ | `notifyStateChanged(state)` | App code calls this on each meaningful change; re-runs pending `waitFor` predicates and logs a `state` entry. |
1575
+ | `waitFor(predicate, { timeoutMs? })` | Resolves with the state the first time `predicate(state)` is true (checked immediately, then on every `notifyStateChanged`). Rejects on timeout. |
1576
+ | `log` | Ring buffer of the last 200 entries: `{ t, kind: 'action' \| 'state' \| 'context', name?, data? }`. Only **dispatched** actions are logged (dropped ones are not). |
1577
+
1578
+ ### 17.3 App-side wiring (once per game)
1579
+
1580
+ ```ts
1581
+ // in the main scene's initialize(), or wherever the game state lives
1582
+ this.app.automation?.registerStateGetter(() => this.store.snapshot());
1583
+ this.store.onChanged.connect((s) => this.app.automation?.notifyStateChanged(s));
1584
+ ```
1585
+
1586
+ Keep the snapshot plain data (no display objects) so predicates stay cheap and
1587
+ serializable for drivers outside the page.
1588
+
1589
+ ### 17.4 Driving it
1590
+
1591
+ Use a browser you own. Launch a separate Chromium with Playwright (isolated,
1592
+ throwaway profile) and point it at the dev server; do **not** attach to or drive the
1593
+ human's own browser session unless they ask you to look at one of their tabs.
1594
+
1595
+ ```ts
1596
+ import { chromium } from 'playwright';
1597
+ const browser = await chromium.launch(); // headless; { headless: false } only when a human watches
1598
+ const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
1599
+ page.on('pageerror', (e) => console.error('pageerror', e));
1600
+ await page.goto('http://localhost:3000/');
1601
+ await page.waitForFunction(() => window.Caper?.__readyApps?.size > 0);
1602
+ const result = await page.evaluate(async () => {
1603
+ const app = await Caper.ready();
1604
+ const a = Caper.automation[app.config.id];
1605
+ a.action('fire');
1606
+ const s = await a.waitFor((st) => st?.phase === 'resolved', { timeoutMs: 10_000 });
1607
+ return { context: a.getContext(), state: s, actions: a.log.filter((e) => e.kind === 'action').map((e) => e.name) };
1608
+ });
1609
+ await page.screenshot({ path: 'scratch/after-fire.png' });
1610
+ await browser.close();
1611
+ ```
1612
+
1613
+ Rules of thumb:
1614
+
1615
+ - Prefer `action()` over synthesising pointer events — it goes through the same
1616
+ ActionsPlugin path as a real button, so context gating and action signals behave
1617
+ identically.
1618
+ - If an action seems ignored, check `getContext()` first: it was probably dropped
1619
+ as out-of-context. That is by design (see §1 / gotchas), not a bug.
1620
+ - `waitFor` only re-evaluates on `notifyStateChanged`; if it never resolves, the
1621
+ app is not calling `notifyStateChanged` for that transition.
1622
+ - For a boot-only smoke test you need none of the facade: assert
1623
+ `Caper.apps.size > 0` after `Caper.ready()` and that the page logged no errors.
1624
+
1625
+ ---
1626
+
1627
+ ## 18. API reference index
1547
1628
 
1548
1629
  Flat jump table. Name → section → one-line.
1549
1630
 
@@ -1604,5 +1685,5 @@ Flat jump table. Name → section → one-line.
1604
1685
 
1605
1686
  ---
1606
1687
 
1607
- **Last updated:** `@caperjs/core@6.2.2` (PixiJS 8.x). When in doubt, read the
1688
+ **Last updated:** with `@caperjs/core@0.5.x` (PixiJS 8.x); version in `../package.json`. When in doubt, read the
1608
1689
  cited framework source file — signatures and defaults are authoritative there.
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: caper
3
+ description: Work on a game built with the Caper engine (@caperjs/core, PixiJS v8). Use before touching scenes, entities, popups, UI, plugins, caper.config.ts, assets, generated types, or the dev/verify loop in any app that depends on @caperjs/core. Routes you to the exact section of the shipped reference (llms.txt) instead of the whole engine, and gives the verify loop (typecheck, build, headless automation bridge). Trigger for "caper", "add a scene/entity/popup/plugin", "factory methods", "add.sprite", "defineScene", "actions", "UICanvas", "FlexContainer", "caper.config", "caper-app.d.ts", "assetpack", "window.Caper", or any error mentioning @caperjs.
4
+ ---
5
+
6
+ # Caper — how to work in a Caper app
7
+
8
+ This skill is shipped by `@caperjs/core` and copied here by `caper agent init`.
9
+ Do not edit it; re-run `npx caper agent init` after upgrading caper.
10
+
11
+ ## 1. Read the reference by section, never whole
12
+
13
+ The full consumer reference is `node_modules/@caperjs/core/extras/llms.txt`
14
+ (~1,600 lines). Load only the section you need:
15
+
16
+ ```bash
17
+ grep -n '^## ' node_modules/@caperjs/core/extras/llms.txt # section line numbers
18
+ sed -n '<start>,<end>p' node_modules/@caperjs/core/extras/llms.txt
19
+ ```
20
+
21
+ | You need to… | Read section |
22
+ | --------------------------------------------- | ---------------------------------- |
23
+ | know the rules before writing any code | §1 Rules of engagement |
24
+ | scaffold a scene / entity / popup / plugin | §2.2 CLI, §15 Recipes |
25
+ | boot, `create()`, custom `Application` | §3 |
26
+ | `caper.config.ts`, actions, contexts, data | §4 |
27
+ | scene lifecycle, assets per scene, transitions| §5 |
28
+ | build a display tree (`add.*` / `make.*`) | §6 (full catalog at §6.1) |
29
+ | buttons, flex layout, popups, toasts, HUD | §7 |
30
+ | `app.scenes / actions / popups / audio / …` | §8 |
31
+ | write or fix a plugin | §9 |
32
+ | assets, bundles, AssetPack tags | §11 |
33
+ | signals, store, mixins | §12 |
34
+ | generated types, virtual modules | §14 |
35
+ | drive the running app headlessly | §17 Automation bridge |
36
+ | look up one symbol | §18 API index (name → section) |
37
+
38
+ Engine source also ships: `node_modules/@caperjs/core/src/`. When the doc is
39
+ not enough, open the one file the doc cites, not the whole tree.
40
+
41
+ ## 2. The rules (llms.txt §1, condensed)
42
+
43
+ 1. `caper.config.ts` `plugins: [...]` is authoritative; list every plugin,
44
+ including ones other plugins `require`.
45
+ 2. Fail loud. No silent fallbacks in plugins or config.
46
+ 3. Metadata goes on `defineScene / defineEntity / definePopup / definePlugin`
47
+ wrappers, not class statics. Default-export the class so discovery finds it.
48
+ 4. `this.add.*` / `this.make.*` over `new Sprite()` / `new Text()` /
49
+ `new Graphics()` / `new Container()`. Extend Caper's `Container`, not Pixi's.
50
+ 5. `npx caper add scene|entity|popup|plugin <Name>` before hand-authoring.
51
+ 6. Verify in the running app, not in isolation (see §3 below).
52
+
53
+ ## 3. Verify loop
54
+
55
+ ```bash
56
+ pnpm typecheck # or the app's tsc script; first gate after any .ts change
57
+ pnpm build # vite build; catches asset and discovery errors
58
+ ```
59
+
60
+ Then check behaviour live, cheapest first:
61
+
62
+ - **Use your own browser, never the human's.** Launch a separate Chromium with
63
+ Playwright (a script via `npx tsx`, or the Playwright MCP tools) pointed at the
64
+ dev-server URL. Do **not** attach to or drive the human's own Chrome (for
65
+ example claude-in-chrome tabs) unless they ask you to look at a tab of theirs.
66
+ Headless for checks; headed (`headless: false`) only when a human will watch.
67
+
68
+ ```ts
69
+ import { chromium } from 'playwright';
70
+ const browser = await chromium.launch(); // isolated, throwaway profile
71
+ const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
72
+ page.on('pageerror', (e) => console.error('pageerror', e));
73
+ await page.goto('http://localhost:3000/');
74
+ await page.waitForFunction(() => window.Caper?.__readyApps?.size > 0);
75
+ await page.screenshot({ path: 'scratch/after-change.png' });
76
+ await browser.close();
77
+ ```
78
+
79
+ - **Headless via the automation bridge** (dev server, or `automation: true`
80
+ in config, or `VITE_CAPER_AUTOMATION=true`): from Playwright or a browser
81
+ tool, `const app = await Caper.ready(); const a = Caper.automation[app.config.id];`
82
+ then `a.action('name', data)`, `a.getContext()`, `a.getState()`,
83
+ `await a.waitFor(s => …, { timeoutMs })`, and inspect `a.log` (last 200
84
+ actions / context changes / state pushes). Details: llms.txt §17.
85
+ - **Do not start the dev server yourself** and leave it running in the
86
+ foreground; it never exits. If the human has it running, use it. Otherwise
87
+ ask them to run `pnpm dev` and report.
88
+
89
+ ## 4. Gotchas that cost agents the most time
90
+
91
+ - Out-of-context actions are **dropped by the ActionsPlugin**. That is the
92
+ phase guard; do not add `if (phase !== …)` checks around `sendAction`.
93
+ - `src/types/caper-app.d.ts` and `caper-assets.d.ts` are **generated** on dev
94
+ server start. Never hand-edit. Missing or stale → delete them and have the
95
+ dev server run once. They are usually gitignored, so a fresh clone has no
96
+ typed ids until then.
97
+ - Weird asset / name mismatches → `rm -rf .assetpack .cache dist` and rebuild.
98
+ - Renamed a scene / popup / entity id? Regenerate types (above) or old ids
99
+ linger in the unions and cause confusing downstream errors.
100
+ - If the engine is **linked from a local checkout**, consumer apps resolve the
101
+ built `lib/`; rebuild the engine (`pnpm framework:build` in the caper repo)
102
+ after editing engine source, unless the app's vite config aliases
103
+ `@caperjs/core` to the engine `src/`.
package/lib/caper.mjs CHANGED
@@ -204,7 +204,7 @@ function B(e, t) {
204
204
  }
205
205
  //#endregion
206
206
  //#region src/version.ts
207
- var V = "0.5.0", H = "8.19.0", Mn = " ........ ..... ............ ............. ........... \n ............. ....... ................ ............. .............. \n .............. ......... ............... ............. .............. \n ..... ........... .............. ............ .............. \n ..... ...... ...... .............. ............. ............. \n .............. ...... ..... ............ ............. ..... ...... \n ................... ..... ..... ............. ..... ....... \n ................ .......... ............. ..... ..... ";
207
+ var V = "0.5.2", H = "8.19.0", Mn = " ........ ..... ............ ............. ........... \n ............. ....... ................ ............. .............. \n .............. ......... ............... ............. .............. \n ..... ........... .............. ............ .............. \n ..... ...... ...... .............. ............. ............. \n .............. ...... ..... ............ ............. ..... ...... \n ................... ..... ..... ............. ..... ....... \n ................ .......... ............. ..... ..... ";
208
208
  function U() {
209
209
  let e = `\n${Mn}\n\n v${V} | %cPixi.js v${H} %c| %chttps://github.com/anthonysapp/caper\n\n`;
210
210
  console.log(e, "color: #E91E63; font-weight: 600;", "color: inherit;", "color: #00BCD4; text-decoration: underline;");