incanto 0.1.0

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.
Files changed (138) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.md +88 -0
  4. package/assets/audio/attacked.mp3 +0 -0
  5. package/assets/audio/explosion.mp3 +0 -0
  6. package/assets/audio/gold_loot.mp3 +0 -0
  7. package/assets/audio/heal.mp3 +0 -0
  8. package/assets/audio/hit_metal_bang.mp3 +0 -0
  9. package/assets/audio/ice_spear.mp3 +0 -0
  10. package/assets/audio/monster_died.mp3 +0 -0
  11. package/assets/audio/slash.mp3 +0 -0
  12. package/assets/audio/smite.mp3 +0 -0
  13. package/assets/audio/spells_cast.mp3 +0 -0
  14. package/assets/audio/ui_click.wav +0 -0
  15. package/assets/audio/walk.mp3 +0 -0
  16. package/assets/catalog.json +390 -0
  17. package/assets/characters/2dbasic.json +41 -0
  18. package/assets/characters/2dbasic.png +0 -0
  19. package/assets/characters/ghost.json +46 -0
  20. package/assets/characters/ghost.png +0 -0
  21. package/assets/characters/goblin.json +40 -0
  22. package/assets/characters/goblin.png +0 -0
  23. package/assets/characters/medieval-knight.json +41 -0
  24. package/assets/characters/medieval-knight.png +0 -0
  25. package/assets/effects/swoosh.png +0 -0
  26. package/assets/items/box.png +0 -0
  27. package/assets/items/buff_potion.png +0 -0
  28. package/assets/items/coin.png +0 -0
  29. package/assets/items/gem.png +0 -0
  30. package/assets/items/gold.png +0 -0
  31. package/assets/items/hp_potion.png +0 -0
  32. package/assets/items/locked_item_box.png +0 -0
  33. package/assets/items/map.png +0 -0
  34. package/assets/items/resurrection_potion.png +0 -0
  35. package/assets/items/super_box.png +0 -0
  36. package/assets/items/trap.png +0 -0
  37. package/assets/tiles/floor00.jpg +0 -0
  38. package/assets/tiles/minecraft-tiles.png +0 -0
  39. package/assets/tiles/wall00.jpg +0 -0
  40. package/assets/vegetation/ash_color.png +0 -0
  41. package/assets/vegetation/aspen_color.png +0 -0
  42. package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
  43. package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
  44. package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
  45. package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
  46. package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
  47. package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
  48. package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
  49. package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
  50. package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
  51. package/assets/vegetation/ground/dirt_color.jpg +0 -0
  52. package/assets/vegetation/ground/dirt_normal.jpg +0 -0
  53. package/assets/vegetation/ground/grass.jpg +0 -0
  54. package/assets/vegetation/oak_color.png +0 -0
  55. package/assets/vegetation/pine_color.png +0 -0
  56. package/bin/incanto-assets.mjs +107 -0
  57. package/bin/incanto-check.mjs +107 -0
  58. package/bin/incanto-editor.mjs +343 -0
  59. package/bin/incanto-env.mjs +144 -0
  60. package/bin/incanto-model.mjs +296 -0
  61. package/bin/incanto-play.mjs +219 -0
  62. package/bin/incanto-skills.mjs +71 -0
  63. package/dist/2d.d.ts +642 -0
  64. package/dist/2d.js +44 -0
  65. package/dist/3d.d.ts +1860 -0
  66. package/dist/3d.js +5 -0
  67. package/dist/agent8-DzU2fFyH.js +129 -0
  68. package/dist/audio-player-DqUR3XFs.d.ts +110 -0
  69. package/dist/behavior-BAQq7HGM.d.ts +851 -0
  70. package/dist/create-game-BdjpTHrW.js +1725 -0
  71. package/dist/create-game-CZHROKcT.js +527 -0
  72. package/dist/debug-draw-CZmOYjL2.js +13 -0
  73. package/dist/debug.d.ts +66 -0
  74. package/dist/debug.js +658 -0
  75. package/dist/duplicate-DP2WPYom.js +22 -0
  76. package/dist/env.d.ts +430 -0
  77. package/dist/env.js +3152 -0
  78. package/dist/errors-BMFaY68Q.d.ts +33 -0
  79. package/dist/errors-BpWbnbb_.js +13 -0
  80. package/dist/gameplay-Ccruc3Wd.js +1501 -0
  81. package/dist/gameplay.d.ts +543 -0
  82. package/dist/gameplay.js +2 -0
  83. package/dist/heightmap-CroQPEER.js +185 -0
  84. package/dist/index.d.ts +305 -0
  85. package/dist/index.js +62 -0
  86. package/dist/json-BLk7H2Qa.js +30 -0
  87. package/dist/loader-CGs_G-r0.js +919 -0
  88. package/dist/loader-Mo0KghCv.d.ts +41 -0
  89. package/dist/net.d.ts +427 -0
  90. package/dist/net.js +772 -0
  91. package/dist/noise-CGUMx44x.js +82 -0
  92. package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
  93. package/dist/particle-sim-DYuSUxvK.js +1319 -0
  94. package/dist/physics-2d-KuMWPTf6.js +288 -0
  95. package/dist/physics-3d-Dl67vOLT.js +434 -0
  96. package/dist/react.d.ts +65 -0
  97. package/dist/react.js +209 -0
  98. package/dist/register-BuUV1_KB.js +561 -0
  99. package/dist/register-CNlYAS1_.js +10634 -0
  100. package/dist/register-DPEV9_9t.js +851 -0
  101. package/dist/register-Dasmnurl.js +374 -0
  102. package/dist/registry-BVJ2HbCn.js +132 -0
  103. package/dist/rng-DP-SR7eg.js +38 -0
  104. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  105. package/dist/schema-CcoWb32N.d.ts +104 -0
  106. package/dist/test.d.ts +158 -0
  107. package/dist/test.js +275 -0
  108. package/dist/touch-031PxtCR.js +208 -0
  109. package/dist/vite.d.ts +26 -0
  110. package/dist/vite.js +57 -0
  111. package/editor/assets/GameServer-C56iOUgF.js +1 -0
  112. package/editor/assets/agent8-Bp7QFI7v.js +1 -0
  113. package/editor/assets/index-DF3tMeKJ.css +1 -0
  114. package/editor/assets/index-Dl2pjA8e.js +7365 -0
  115. package/editor/assets/rapier-CEuLKeCu.js +1 -0
  116. package/editor/assets/rapier-DE6a0vmv.js +1 -0
  117. package/editor/index.html +169 -0
  118. package/package.json +97 -0
  119. package/schemas/scene.schema.json +4254 -0
  120. package/skills/README.md +9 -0
  121. package/skills/incanto-3d-character.md +229 -0
  122. package/skills/incanto-3d-models.md +151 -0
  123. package/skills/incanto-assets.md +118 -0
  124. package/skills/incanto-audio.md +309 -0
  125. package/skills/incanto-behaviors-and-scripts.md +169 -0
  126. package/skills/incanto-building-2d-games.md +242 -0
  127. package/skills/incanto-building-3d-games.md +245 -0
  128. package/skills/incanto-editor.md +163 -0
  129. package/skills/incanto-environment.md +743 -0
  130. package/skills/incanto-gameplay-behaviors.md +707 -0
  131. package/skills/incanto-multiplayer.md +264 -0
  132. package/skills/incanto-node-reference.md +797 -0
  133. package/skills/incanto-physics-and-input.md +164 -0
  134. package/skills/incanto-scene-json-authoring.md +325 -0
  135. package/skills/incanto-verifying-your-game.md +191 -0
  136. package/skills/incanto-web-integration.md +96 -0
  137. package/templates/agent8-server.js +84 -0
  138. package/templates/agent8-server.ts +138 -0
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * incanto-model — inspect a GLB/glTF/VRM file and report what an agent cannot
4
+ * guess from the file name: node hierarchy, mesh stats, the TRANSFORMED scene
5
+ * bounding box (size/center), animations, materials, and VRM meta/humanoid.
6
+ *
7
+ * npx incanto-model character.vrm # human summary
8
+ * npx incanto-model level.glb --json # full machine-readable report
9
+ *
10
+ * Bounding boxes come from glTF POSITION accessor min/max transformed through
11
+ * each node's TRS chain — for skinned meshes that is the BIND pose.
12
+ */
13
+ import { readFileSync } from 'node:fs';
14
+
15
+ function parseArgs(argv) {
16
+ const args = { file: undefined, json: false };
17
+ for (const a of argv) {
18
+ if (a === '--json') args.json = true;
19
+ else if (a === '--help' || a === '-h') args.help = true;
20
+ else if (!a.startsWith('-') && !args.file) args.file = a;
21
+ else {
22
+ console.error(`unknown argument: ${a}`);
23
+ args.error = true;
24
+ }
25
+ }
26
+ return args;
27
+ }
28
+
29
+ const args = parseArgs(process.argv.slice(2));
30
+ if (args.help || args.error || !args.file) {
31
+ console.log(`Usage: npx incanto-model <file.glb|.gltf|.vrm> [--json]
32
+
33
+ Reports hierarchy, mesh stats, scene bounding box, animations, and VRM meta —
34
+ the numbers you need to size and place a model in a scene.`);
35
+ process.exit(args.help && !args.error ? 0 : 1);
36
+ }
37
+
38
+ // ---- container parsing --------------------------------------------------------------
39
+
40
+ function readGltfJson(file) {
41
+ const data = readFileSync(file);
42
+ if (data.length >= 12 && data.readUInt32LE(0) === 0x46546c67) {
43
+ // GLB container: header(12) then chunks [len][type][payload]
44
+ let offset = 12;
45
+ while (offset + 8 <= data.length) {
46
+ const len = data.readUInt32LE(offset);
47
+ const type = data.readUInt32LE(offset + 4);
48
+ if (type === 0x4e4f534a) {
49
+ return JSON.parse(data.subarray(offset + 8, offset + 8 + len).toString('utf-8'));
50
+ }
51
+ offset += 8 + len + ((4 - (len % 4)) % 4);
52
+ }
53
+ throw new Error('GLB has no JSON chunk');
54
+ }
55
+ return JSON.parse(data.toString('utf-8')); // raw .gltf
56
+ }
57
+
58
+ // ---- minimal mat4 (column-major, like glTF) -------------------------------------------
59
+
60
+ const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
61
+
62
+ function composeTrs(node) {
63
+ if (node.matrix) return node.matrix;
64
+ const [tx, ty, tz] = node.translation ?? [0, 0, 0];
65
+ const [qx, qy, qz, qw] = node.rotation ?? [0, 0, 0, 1];
66
+ const [sx, sy, sz] = node.scale ?? [1, 1, 1];
67
+ const x2 = qx + qx,
68
+ y2 = qy + qy,
69
+ z2 = qz + qz;
70
+ const xx = qx * x2,
71
+ xy = qx * y2,
72
+ xz = qx * z2;
73
+ const yy = qy * y2,
74
+ yz = qy * z2,
75
+ zz = qz * z2;
76
+ const wx = qw * x2,
77
+ wy = qw * y2,
78
+ wz = qw * z2;
79
+ return [
80
+ (1 - (yy + zz)) * sx,
81
+ (xy + wz) * sx,
82
+ (xz - wy) * sx,
83
+ 0,
84
+ (xy - wz) * sy,
85
+ (1 - (xx + zz)) * sy,
86
+ (yz + wx) * sy,
87
+ 0,
88
+ (xz + wy) * sz,
89
+ (yz - wx) * sz,
90
+ (1 - (xx + yy)) * sz,
91
+ 0,
92
+ tx,
93
+ ty,
94
+ tz,
95
+ 1,
96
+ ];
97
+ }
98
+
99
+ function mulMat4(a, b) {
100
+ const out = new Array(16);
101
+ for (let c = 0; c < 4; c++) {
102
+ for (let r = 0; r < 4; r++) {
103
+ out[c * 4 + r] =
104
+ a[r] * b[c * 4] +
105
+ a[4 + r] * b[c * 4 + 1] +
106
+ a[8 + r] * b[c * 4 + 2] +
107
+ a[12 + r] * b[c * 4 + 3];
108
+ }
109
+ }
110
+ return out;
111
+ }
112
+
113
+ function transformPoint(m, [x, y, z]) {
114
+ return [
115
+ m[0] * x + m[4] * y + m[8] * z + m[12],
116
+ m[1] * x + m[5] * y + m[9] * z + m[13],
117
+ m[2] * x + m[6] * y + m[10] * z + m[14],
118
+ ];
119
+ }
120
+
121
+ // ---- analysis ------------------------------------------------------------------------
122
+
123
+ function round(n) {
124
+ return Math.round(n * 1000) / 1000;
125
+ }
126
+
127
+ function bboxOf(min, max) {
128
+ return {
129
+ min: min.map(round),
130
+ max: max.map(round),
131
+ size: max.map((v, i) => round(v - min[i])),
132
+ center: max.map((v, i) => round((v + min[i]) / 2)),
133
+ };
134
+ }
135
+
136
+ function analyze(json) {
137
+ const meshes = (json.meshes ?? []).map((mesh) => {
138
+ let vertices = 0;
139
+ let triangles = 0;
140
+ let min = null;
141
+ let max = null;
142
+ for (const prim of mesh.primitives ?? []) {
143
+ const pos = json.accessors?.[prim.attributes?.POSITION];
144
+ if (pos) {
145
+ vertices += pos.count ?? 0;
146
+ if (pos.min && pos.max) {
147
+ min = min ? min.map((v, i) => Math.min(v, pos.min[i])) : [...pos.min];
148
+ max = max ? max.map((v, i) => Math.max(v, pos.max[i])) : [...pos.max];
149
+ }
150
+ }
151
+ const idx = json.accessors?.[prim.indices];
152
+ const drawn = idx?.count ?? pos?.count ?? 0;
153
+ const mode = prim.mode ?? 4; // TRIANGLES default
154
+ if (mode === 4) triangles += Math.floor(drawn / 3);
155
+ else if (mode === 5 || mode === 6) triangles += Math.max(0, drawn - 2); // strip/fan
156
+ }
157
+ return {
158
+ name: mesh.name ?? '(unnamed mesh)',
159
+ primitives: (mesh.primitives ?? []).length,
160
+ vertices,
161
+ triangles,
162
+ bbox: min && max ? bboxOf(min, max) : null,
163
+ localMin: min,
164
+ localMax: max,
165
+ };
166
+ });
167
+
168
+ // hierarchy + scene bbox (transform each mesh's 8 corners through the TRS chain)
169
+ const nodes = [];
170
+ let sceneMin = null;
171
+ let sceneMax = null;
172
+ const seen = new Set(); // corrupt files can contain cycles — never recurse forever
173
+ const visit = (index, depth, parentMat) => {
174
+ if (seen.has(index) || depth > 256) return;
175
+ seen.add(index);
176
+ const node = json.nodes?.[index];
177
+ if (!node) return;
178
+ const mat = mulMat4(parentMat, composeTrs(node));
179
+ const entry = {
180
+ name: node.name ?? `(node ${index})`,
181
+ depth,
182
+ children: (node.children ?? []).length,
183
+ };
184
+ if (node.mesh !== undefined)
185
+ entry.mesh = meshes[node.mesh]?.name ?? `(missing mesh #${node.mesh})`;
186
+ if (node.skin !== undefined) entry.skinned = true;
187
+ nodes.push(entry);
188
+ const mesh = node.mesh !== undefined ? meshes[node.mesh] : null;
189
+ if (mesh?.localMin && mesh.localMax) {
190
+ for (const x of [mesh.localMin[0], mesh.localMax[0]]) {
191
+ for (const y of [mesh.localMin[1], mesh.localMax[1]]) {
192
+ for (const z of [mesh.localMin[2], mesh.localMax[2]]) {
193
+ const p = transformPoint(mat, [x, y, z]);
194
+ sceneMin = sceneMin ? sceneMin.map((v, i) => Math.min(v, p[i])) : [...p];
195
+ sceneMax = sceneMax ? sceneMax.map((v, i) => Math.max(v, p[i])) : [...p];
196
+ }
197
+ }
198
+ }
199
+ }
200
+ for (const child of node.children ?? []) visit(child, depth + 1, mat);
201
+ };
202
+ const sceneIndex = json.scene ?? 0;
203
+ for (const rootIndex of json.scenes?.[sceneIndex]?.nodes ?? []) visit(rootIndex, 0, IDENTITY);
204
+
205
+ const animations = (json.animations ?? []).map((anim) => {
206
+ let duration = 0;
207
+ for (const sampler of anim.samplers ?? []) {
208
+ const input = json.accessors?.[sampler.input];
209
+ if (input?.max?.[0] !== undefined) duration = Math.max(duration, input.max[0]);
210
+ }
211
+ return { name: anim.name ?? '(unnamed)', duration: round(duration) };
212
+ });
213
+
214
+ let vrm = null;
215
+ if (json.extensions?.VRM) {
216
+ const v = json.extensions.VRM;
217
+ vrm = {
218
+ spec: '0.x',
219
+ name: v.meta?.title ?? null,
220
+ authors: v.meta?.author ? [v.meta.author] : [],
221
+ humanBones: (v.humanoid?.humanBones ?? []).length,
222
+ };
223
+ } else if (json.extensions?.VRMC_vrm) {
224
+ const v = json.extensions.VRMC_vrm;
225
+ vrm = {
226
+ spec: '1.0',
227
+ name: v.meta?.name ?? null,
228
+ authors: v.meta?.authors ?? [],
229
+ humanBones: Object.keys(v.humanoid?.humanBones ?? {}).length,
230
+ };
231
+ }
232
+
233
+ return {
234
+ generator: json.asset?.generator ?? null,
235
+ nodes,
236
+ meshes: meshes.map(({ localMin, localMax, ...rest }) => rest),
237
+ bbox: sceneMin && sceneMax ? bboxOf(sceneMin, sceneMax) : null,
238
+ animations,
239
+ materials: (json.materials ?? []).map((m, i) => m.name ?? `(material ${i})`),
240
+ textures: (json.textures ?? []).length,
241
+ skins: (json.skins ?? []).length,
242
+ vrm,
243
+ };
244
+ }
245
+
246
+ // ---- output --------------------------------------------------------------------------
247
+
248
+ let report;
249
+ try {
250
+ const json = readGltfJson(args.file);
251
+ if (!json.asset?.version) throw new Error('missing glTF asset header');
252
+ report = {
253
+ file: args.file,
254
+ format: readFileSync(args.file).readUInt32LE(0) === 0x46546c67 ? 'glb' : 'gltf',
255
+ ...analyze(json),
256
+ };
257
+ } catch (error) {
258
+ console.error(`not a glTF/GLB/VRM file: ${String(error.message ?? error)}`);
259
+ process.exit(1);
260
+ }
261
+
262
+ if (args.json) {
263
+ console.log(JSON.stringify(report, null, 2));
264
+ } else {
265
+ const out = [];
266
+ out.push(`${report.file} (${report.format}${report.vrm ? ` · VRM ${report.vrm.spec}` : ''})`);
267
+ if (report.vrm) {
268
+ out.push(
269
+ ` avatar: ${report.vrm.name ?? '?'} by ${report.vrm.authors.join(', ') || '?'} · ${report.vrm.humanBones} humanoid bones`,
270
+ );
271
+ }
272
+ if (report.bbox) {
273
+ out.push(
274
+ ` size: ${report.bbox.size.join(' × ')} center: [${report.bbox.center.join(', ')}]`,
275
+ );
276
+ out.push(` bounds: [${report.bbox.min.join(', ')}] → [${report.bbox.max.join(', ')}]`);
277
+ }
278
+ out.push(
279
+ ` nodes: ${report.nodes.length} · skins: ${report.skins} · materials: ${report.materials.length} · textures: ${report.textures}`,
280
+ );
281
+ for (const node of report.nodes.slice(0, 40)) {
282
+ out.push(
283
+ ` ${' '.repeat(node.depth)}${node.name}${node.mesh ? ` ▸ ${node.mesh}` : ''}${node.skinned ? ' (skinned)' : ''}`,
284
+ );
285
+ }
286
+ if (report.nodes.length > 40) out.push(` … ${report.nodes.length - 40} more`);
287
+ for (const mesh of report.meshes) {
288
+ out.push(
289
+ ` mesh ${mesh.name}: ${mesh.vertices} verts · ${mesh.triangles} tris${mesh.bbox ? ` · size ${mesh.bbox.size.join('×')}` : ''}`,
290
+ );
291
+ }
292
+ for (const anim of report.animations) {
293
+ out.push(` anim ${anim.name}: ${anim.duration}s`);
294
+ }
295
+ console.log(out.join('\n'));
296
+ }
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ /**
5
+ * incanto-play — play a scene HEADLESSLY from bash. The agent's gamepad:
6
+ * feed inputs over stdin, advance simulated time, and capture the CURRENT
7
+ * state as scene JSON — the same format as a saved *.scene.json, so reading
8
+ * a capture is reading the screen.
9
+ *
10
+ * bunx incanto-play src/game.scene.json --behaviors src/behaviors.ts
11
+ *
12
+ * stdin protocol (one command per line; every response is one JSON line):
13
+ * step <ms> advance the simulation
14
+ * press <action> hold a button action release <action>
15
+ * vector <action> <x> <y> analog direction ((0 0) clears)
16
+ * key <code> down|up raw KeyboardEvent.code mouse <0|1|2> down|up
17
+ * pointer <dx> <dy> look deltas wheel <dy>
18
+ * capture [file] current state as scene JSON (stdout or file)
19
+ * describe one line per node, non-default props only
20
+ * logs engine.log entries since the last call
21
+ * quit
22
+ *
23
+ * Without --behaviors, unregistered scripts are stubbed (structure-only play).
24
+ * --behaviors accepts .ts directly (node 23.6+ type stripping / bun).
25
+ */
26
+ import { createInterface } from 'node:readline';
27
+ import { fileURLToPath, pathToFileURL } from 'node:url';
28
+
29
+ const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
30
+
31
+ function parseArgs(argv) {
32
+ const args = {
33
+ scene: undefined,
34
+ behaviors: undefined,
35
+ seed: undefined,
36
+ fixedHz: undefined,
37
+ commands: undefined,
38
+ };
39
+ for (let i = 0; i < argv.length; i++) {
40
+ const a = argv[i];
41
+ if (a === '--behaviors') args.behaviors = argv[++i];
42
+ else if (a === '--seed') args.seed = Number(argv[++i]);
43
+ else if (a === '--fixed-hz') args.fixedHz = Number(argv[++i]);
44
+ else if (a === '--commands') args.commands = argv[++i];
45
+ else if (a === '--help' || a === '-h') args.help = true;
46
+ else if (a.startsWith('--')) {
47
+ console.error(`unknown flag: ${a}`);
48
+ args.help = true;
49
+ } else if (!args.scene) args.scene = a;
50
+ }
51
+ return args;
52
+ }
53
+
54
+ const args = parseArgs(process.argv.slice(2));
55
+ if (args.help || !args.scene) {
56
+ console.error(`Usage: incanto-play <scene.json> [--behaviors <file.ts|.js>] [--seed N] [--fixed-hz 60] [--commands <file>]
57
+
58
+ Headless play: stdin commands (step/press/release/vector/key/mouse/pointer/
59
+ wheel/capture/describe/logs/quit), JSON-line responses on stdout.`);
60
+ process.exit(args.help && args.scene !== undefined ? 0 : 1);
61
+ }
62
+
63
+ const out = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`);
64
+ const fail = (cmd, e) =>
65
+ out({ ok: false, cmd, error: { code: e?.code ?? 'ERROR', message: e?.message ?? String(e) } });
66
+
67
+ const { createPlaySession } = await import(pathToFileURL(join(PKG, 'dist', 'test.js')).href);
68
+ const incanto = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
69
+
70
+ // --behaviors: import the consumer module (TS works on node >= 23.6 / bun)
71
+ // and register every exported Behavior subclass under its export name.
72
+ const behaviors = {};
73
+ if (args.behaviors) {
74
+ let mod;
75
+ try {
76
+ mod = await import(pathToFileURL(resolve(args.behaviors)).href);
77
+ } catch (e) {
78
+ const reason =
79
+ e?.code === 'ERR_MODULE_NOT_FOUND' && String(e.message).includes(resolve(args.behaviors))
80
+ ? 'file not found'
81
+ : (e?.message ?? String(e));
82
+ out({
83
+ ok: false,
84
+ cmd: 'behaviors',
85
+ error: {
86
+ code: e?.code ?? 'BEHAVIORS_LOAD_FAILED',
87
+ message: `could not load --behaviors '${args.behaviors}' (${reason}). Pass the file that exports your Behavior subclasses, e.g. src/behaviors.ts.`,
88
+ },
89
+ });
90
+ process.exit(1);
91
+ }
92
+ for (const [name, value] of Object.entries(mod)) {
93
+ if (typeof value === 'function' && value.prototype instanceof incanto.Behavior) {
94
+ behaviors[name] = value;
95
+ }
96
+ }
97
+ if (Object.keys(behaviors).length === 0) {
98
+ console.error(`warning: no Behavior subclasses exported from ${args.behaviors}`);
99
+ }
100
+ }
101
+
102
+ const sceneJson = JSON.parse(readFileSync(resolve(args.scene), 'utf-8'));
103
+ let session;
104
+ try {
105
+ session = await createPlaySession(sceneJson, {
106
+ behaviors,
107
+ stubMissingBehaviors: !args.behaviors, // structure-only play without code
108
+ seed: args.seed,
109
+ fixedHz: args.fixedHz,
110
+ resolveScene: (p) =>
111
+ JSON.parse(readFileSync(resolve(dirname(resolve(args.scene)), p), 'utf-8')),
112
+ });
113
+ } catch (e) {
114
+ fail('load', e);
115
+ process.exit(1);
116
+ }
117
+
118
+ out({
119
+ ok: true,
120
+ ready: true,
121
+ scene: session.scene.name,
122
+ dimension: session.scene.dimension ?? null,
123
+ behaviors: Object.keys(behaviors),
124
+ stubbed: !args.behaviors,
125
+ });
126
+
127
+ function handle(line) {
128
+ const [cmd, ...parts] = line.trim().split(/\s+/);
129
+ if (!cmd) return true;
130
+ try {
131
+ switch (cmd) {
132
+ case 'step': {
133
+ session.step(Number(parts[0] ?? 16.7));
134
+ out({ ok: true, cmd, t: Math.round(session.timeMs) });
135
+ break;
136
+ }
137
+ case 'press':
138
+ session.engine.input.pressAction(parts[0]);
139
+ out({ ok: true, cmd, action: parts[0] });
140
+ break;
141
+ case 'release':
142
+ session.engine.input.releaseAction(parts[0]);
143
+ out({ ok: true, cmd, action: parts[0] });
144
+ break;
145
+ case 'vector':
146
+ session.engine.input.setActionVector(parts[0], Number(parts[1]), Number(parts[2]));
147
+ out({ ok: true, cmd, action: parts[0] });
148
+ break;
149
+ case 'key':
150
+ session.engine.input.handleKey(parts[0], parts[1] !== 'up');
151
+ out({ ok: true, cmd, code: parts[0] });
152
+ break;
153
+ case 'mouse':
154
+ session.engine.input.handleMouseButton(Number(parts[0]), parts[1] !== 'up');
155
+ out({ ok: true, cmd });
156
+ break;
157
+ case 'pointer':
158
+ session.engine.input.handlePointerMove(Number(parts[0]), Number(parts[1]));
159
+ out({ ok: true, cmd });
160
+ break;
161
+ case 'wheel':
162
+ session.engine.input.handleWheel(Number(parts[0]));
163
+ out({ ok: true, cmd });
164
+ break;
165
+ case 'capture': {
166
+ const scene = session.capture();
167
+ if (parts[0]) {
168
+ writeFileSync(resolve(parts[0]), `${JSON.stringify(scene, null, 2)}\n`);
169
+ out({ ok: true, cmd, t: Math.round(session.timeMs), file: resolve(parts[0]) });
170
+ } else {
171
+ out({ ok: true, cmd, t: Math.round(session.timeMs), scene });
172
+ }
173
+ break;
174
+ }
175
+ case 'describe':
176
+ out({ ok: true, cmd, text: session.describe() });
177
+ break;
178
+ case 'logs':
179
+ out({ ok: true, cmd, entries: session.drainLogs() });
180
+ break;
181
+ case 'quit':
182
+ session.dispose();
183
+ out({ ok: true, cmd });
184
+ return false;
185
+ default:
186
+ out({
187
+ ok: false,
188
+ cmd,
189
+ error: {
190
+ code: 'UNKNOWN_COMMAND',
191
+ message: `unknown command '${cmd}'. Commands: step press release vector key mouse pointer wheel capture describe logs quit`,
192
+ },
193
+ });
194
+ }
195
+ } catch (e) {
196
+ fail(cmd, e);
197
+ }
198
+ return true;
199
+ }
200
+
201
+ if (args.commands) {
202
+ for (const line of readFileSync(resolve(args.commands), 'utf-8').split('\n')) {
203
+ if (!handle(line)) process.exit(0);
204
+ }
205
+ session.dispose();
206
+ process.exit(0);
207
+ }
208
+
209
+ const rl = createInterface({ input: process.stdin });
210
+ rl.on('line', (line) => {
211
+ if (!handle(line)) {
212
+ rl.close();
213
+ process.exit(0);
214
+ }
215
+ });
216
+ rl.on('close', () => {
217
+ session.dispose();
218
+ process.exit(0);
219
+ });
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * incanto-skills — install this package's agent skills into your project so an
4
+ * AI agent launched there can vibe-code games with the INSTALLED engine version.
5
+ *
6
+ * npx incanto-skills --format claude → .claude/skills/<name>/SKILL.md
7
+ * npx incanto-skills --format opencode → skills/<name>.md (flat)
8
+ * npx incanto-skills --format claude --out custom/dir
9
+ *
10
+ * Skills are copied from the installed package (node_modules/incanto/skills),
11
+ * so they always match the engine version your project depends on.
12
+ */
13
+ import { copyFileSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
14
+ import { basename, dirname, join, resolve } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills');
18
+
19
+ function parseArgs(argv) {
20
+ const args = { format: 'claude', out: undefined };
21
+ for (let i = 0; i < argv.length; i++) {
22
+ if (argv[i] === '--format') args.format = argv[++i];
23
+ else if (argv[i] === '--out') args.out = argv[++i];
24
+ else if (argv[i] === '--help' || argv[i] === '-h') args.help = true;
25
+ else {
26
+ console.error(`unknown argument: ${argv[i]}`);
27
+ args.help = true;
28
+ }
29
+ }
30
+ return args;
31
+ }
32
+
33
+ const args = parseArgs(process.argv.slice(2));
34
+ if (args.help || !['claude', 'opencode'].includes(args.format)) {
35
+ console.log(`Usage: npx incanto-skills [--format claude|opencode] [--out <dir>]
36
+
37
+ claude (default) Claude Code project skills:
38
+ <out>/<skill-name>/SKILL.md (default out: .claude/skills)
39
+ opencode flat markdown files:
40
+ <out>/<skill-name>.md (default out: skills)
41
+
42
+ Skills are read from the installed incanto package and match its version.`);
43
+ process.exit(args.help ? 0 : 1);
44
+ }
45
+
46
+ const outRoot = resolve(args.out ?? (args.format === 'claude' ? '.claude/skills' : 'skills'));
47
+ const files = readdirSync(SKILLS_DIR).filter((f) => f.startsWith('incanto-') && f.endsWith('.md'));
48
+ if (files.length === 0) {
49
+ console.error(`no incanto-*.md skills found in ${SKILLS_DIR}`);
50
+ process.exit(1);
51
+ }
52
+
53
+ for (const file of files) {
54
+ const name = basename(file, '.md');
55
+ if (args.format === 'claude') {
56
+ const dir = join(outRoot, name);
57
+ mkdirSync(dir, { recursive: true });
58
+ copyFileSync(join(SKILLS_DIR, file), join(dir, 'SKILL.md'));
59
+ } else {
60
+ mkdirSync(outRoot, { recursive: true });
61
+ copyFileSync(join(SKILLS_DIR, file), join(outRoot, file));
62
+ }
63
+ }
64
+
65
+ // A tiny manifest so tooling (and humans) can see what was installed from where.
66
+ writeFileSync(
67
+ join(outRoot, 'incanto-skills.json'),
68
+ `${JSON.stringify({ source: 'incanto', format: args.format, skills: files.map((f) => basename(f, '.md')) }, null, 2)}\n`,
69
+ );
70
+
71
+ console.log(`installed ${files.length} incanto skills (${args.format} format) → ${outRoot}`);