@inneranimalmedia/agentsam-sdk 2.6.7 → 2.6.9

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 (371) hide show
  1. package/README.md +37 -0
  2. package/apps/README.md +25 -0
  3. package/apps/cad-creator/shared/cad/package.json +1 -1
  4. package/apps/client-cms-editor/backend/package.json +4 -6
  5. package/apps/client-cms-editor/frontend/package.json +6 -8
  6. package/apps/client-cms-editor/frontend/tsconfig.json +27 -8
  7. package/apps/client-cms-editor/package.json +1 -1
  8. package/apps/client-cms-editor/shared/cms/package.json +4 -6
  9. package/docs/LOCAL_ENGINE_COMPUTE_AUDIT_PLAN.md +299 -0
  10. package/docs/REPO_FILE_MAP.txt +132 -0
  11. package/migrations/d1/0018_goap_control_plane_remaster.sql +40 -0
  12. package/migrations/d1/0019_agentsam_agent_run_plan_todo.sql +14 -0
  13. package/migrations/d1/README_PORTABLE_CONTROL_PLANE.md +20 -9
  14. package/migrations/runtime/0002_goap_blackboard_v1.sql +15 -0
  15. package/migrations/runtime/0004_agent_run_plan_linkage.sql +8 -0
  16. package/native/agentsam-machine/cli/Cargo.toml +8 -1
  17. package/native/agentsam-machine/cli/README.md +21 -0
  18. package/native/agentsam-machine/core/Cargo.toml +7 -0
  19. package/native/agentsam-machine/core/README.md +14 -0
  20. package/native/agentsam-machine/core/build.rs +33 -0
  21. package/native/agentsam-machine/core/contracts/source-types.v1.json +353 -0
  22. package/native/agentsam-machine/core/src/lib.rs +6 -0
  23. package/native/agentsam-machine/core/src/source_types.rs +2 -2
  24. package/package.json +31 -10
  25. package/packages/agentsam-brand/package.json +1 -1
  26. package/packages/agentsam-contracts/package.json +71 -20
  27. package/packages/agentsam-contracts/scripts/build.mjs +18 -0
  28. package/packages/agentsam-contracts/test/contracts.test.mjs +4 -1
  29. package/packages/agentsam-contracts/test/repository.test.mjs +4 -1
  30. package/packages/agentsam-contracts/tsconfig.build.json +10 -0
  31. package/packages/agentsam-errors/package.json +3 -2
  32. package/packages/agentsam-errors/src/index.js +1 -0
  33. package/packages/agentsam-errors/src/tool.js +56 -0
  34. package/packages/agentsam-errors/test/tool.test.mjs +58 -0
  35. package/packages/agentsam-goap/README.md +52 -0
  36. package/packages/agentsam-goap/package.json +37 -0
  37. package/packages/agentsam-goap/src/adapters/d1-sqlite.js +570 -0
  38. package/packages/agentsam-goap/src/adapters/memory.js +199 -0
  39. package/packages/agentsam-goap/src/contracts.js +226 -0
  40. package/packages/agentsam-goap/src/control-plane.js +112 -0
  41. package/packages/agentsam-goap/src/errors.js +157 -0
  42. package/packages/agentsam-goap/src/index.js +5 -0
  43. package/packages/agentsam-goap/src/world.js +202 -0
  44. package/packages/agentsam-goap/test/goap-control-plane.test.mjs +245 -0
  45. package/packages/agentsam-goap/test/world.test.mjs +79 -0
  46. package/packages/agentsam-knowledge/package.json +3 -2
  47. package/packages/agentsam-nav/package.json +1 -1
  48. package/packages/agentsam-queue-control/package.json +1 -1
  49. package/packages/agentsam-rapid-rust/src/main.rs +1 -0
  50. package/packages/agentsam-repository/package.json +3 -2
  51. package/packages/agentsam-repository/src/goap.js +13 -14
  52. package/packages/agentsam-repository/src/tickets.js +15 -22
  53. package/packages/agentsam-repository/test/goap-tickets-workspace-state.test.mjs +19 -8
  54. package/packages/agentsam-vault/package.json +8 -5
  55. package/packages/agentsam-workbench/package.json +44 -17
  56. package/packages/connectors/cloudflare/package.json +4 -2
  57. package/packages/heuristic-theme/package.json +1 -1
  58. package/packages/identity/package.json +4 -3
  59. package/packages/identity/tests/lane1-packed-consumer.test.mjs +10 -2
  60. package/packages/providers/completeful/package.json +1 -1
  61. package/packages/theme-church-site/README.md +2 -2
  62. package/packages/theme-church-site/package.json +3 -4
  63. package/packages/theme-church-site/src/index.js +1 -1
  64. package/packages/theme-church-site/test/theme.test.mjs +1 -1
  65. package/packages/theme-companions-site/README.md +2 -2
  66. package/packages/theme-companions-site/package.json +3 -4
  67. package/packages/theme-companions-site/src/index.js +1 -1
  68. package/packages/theme-companions-site/test/theme.test.mjs +1 -1
  69. package/packages/theme-floors-site/README.md +2 -2
  70. package/packages/theme-floors-site/package.json +3 -4
  71. package/packages/theme-floors-site/src/index.js +1 -1
  72. package/packages/theme-floors-site/test/theme.test.mjs +1 -1
  73. package/packages/theme-fuelnfree-site/README.md +2 -2
  74. package/packages/theme-fuelnfree-site/package.json +3 -4
  75. package/packages/theme-fuelnfree-site/src/index.js +1 -1
  76. package/packages/theme-fuelnfree-site/test/theme.test.mjs +1 -1
  77. package/packages/theme-handyman-site/README.md +2 -2
  78. package/packages/theme-handyman-site/package.json +3 -4
  79. package/packages/theme-handyman-site/src/index.js +1 -1
  80. package/packages/theme-handyman-site/test/theme.test.mjs +1 -1
  81. package/packages/theme-insurance-site/README.md +2 -2
  82. package/packages/theme-insurance-site/package.json +3 -4
  83. package/packages/theme-insurance-site/src/index.js +1 -1
  84. package/packages/theme-insurance-site/test/theme.test.mjs +1 -1
  85. package/packages/theme-scenes/package.json +1 -1
  86. package/packages/theme-shinshu-site/README.md +2 -2
  87. package/packages/theme-shinshu-site/package.json +3 -4
  88. package/packages/theme-shinshu-site/src/index.js +1 -1
  89. package/packages/theme-shinshu-site/test/theme.test.mjs +1 -1
  90. package/packages/work-graph/package.json +3 -2
  91. package/protocol/local-engine/benchmark.v1.schema.json +14 -0
  92. package/protocol/local-engine/engine.v1.schema.json +17 -0
  93. package/python/agentsam_sdk/repository/intelligence/discovery.py +27 -2
  94. package/python/agentsam_sdk/repository/intelligence/git.py +4 -0
  95. package/python/tests/test_repository_intelligence.py +30 -0
  96. package/src/cli/command-catalog.js +3 -1
  97. package/src/cli.js +20 -1
  98. package/src/commands/app.js +10 -0
  99. package/src/commands/engine.js +10 -0
  100. package/src/commands/goap-world.js +134 -0
  101. package/src/commands/goap.js +11 -4
  102. package/src/commands/machine-binary.js +33 -27
  103. package/src/commands/machine-runtime.js +266 -0
  104. package/src/commands/machine.js +155 -51
  105. package/src/commands/package.js +537 -0
  106. package/src/commands/plan-ledger.js +370 -0
  107. package/src/commands/plan.js +183 -0
  108. package/src/commands/runtime.js +25 -10
  109. package/src/commands/setup.js +30 -6
  110. package/src/commands/skill.js +78 -17
  111. package/src/commands/terminal.js +20 -8
  112. package/src/errors/contract.js +4 -4
  113. package/src/errors/diagnostic.js +1 -1
  114. package/src/errors/index.js +1 -1
  115. package/src/index.js +20 -0
  116. package/src/indexing/ingest/materials.js +237 -30
  117. package/src/knowledge/source.js +1 -1
  118. package/src/lib/setup/runtime.js +4 -2
  119. package/src/lib/slash-commands.js +1 -1
  120. package/src/local/runtime-store.js +24 -2
  121. package/src/local/sqlite.js +17 -0
  122. package/src/local-engine/assets.js +37 -0
  123. package/src/local-engine/audit.js +133 -0
  124. package/src/local-engine/benchmark.js +26 -0
  125. package/src/local-engine/contracts.js +101 -0
  126. package/src/local-engine/engines.js +142 -0
  127. package/src/local-engine/index.js +18 -0
  128. package/src/local-engine/runtime.js +39 -0
  129. package/src/sam/operations/package-audit.js +41 -0
  130. package/src/sam/seed.js +2 -0
  131. package/skills/grok-auth/SKILL.md +0 -110
  132. package/skills/grok-auth/agentsam.skill.json +0 -25
  133. package/skills/grok-auth/references/grok-identity.md +0 -64
  134. package/skills/grok-auth/references/per-user-data.md +0 -49
  135. package/skills/grok-auth/references/prewired-and-env.md +0 -54
  136. package/skills/grok-auth/references/session-ui.md +0 -133
  137. package/skills/grok-auth/references/sign-in-methods.md +0 -55
  138. package/skills/grok-auth/references/wiring.md +0 -71
  139. package/skills/grok-building-games/SKILL.md +0 -213
  140. package/skills/grok-building-games/agentsam.skill.json +0 -25
  141. package/skills/grok-building-games/references/3d-libs.md +0 -64
  142. package/skills/grok-building-games/references/ai-pathfinding.md +0 -122
  143. package/skills/grok-building-games/references/audio.md +0 -129
  144. package/skills/grok-building-games/references/babylon.md +0 -202
  145. package/skills/grok-building-games/references/collision-physics.md +0 -122
  146. package/skills/grok-building-games/references/ecs-architecture.md +0 -111
  147. package/skills/grok-building-games/references/game-feel-juice.md +0 -110
  148. package/skills/grok-building-games/references/genres/board-card-chess.md +0 -89
  149. package/skills/grok-building-games/references/genres/endless-runner.md +0 -83
  150. package/skills/grok-building-games/references/genres/fps.md +0 -105
  151. package/skills/grok-building-games/references/genres/platformer-2d.md +0 -97
  152. package/skills/grok-building-games/references/genres/puzzle-match3-tetris.md +0 -131
  153. package/skills/grok-building-games/references/genres/racing-kart.md +0 -106
  154. package/skills/grok-building-games/references/genres/topdown-twin-stick.md +0 -97
  155. package/skills/grok-building-games/references/genres/tower-defense.md +0 -84
  156. package/skills/grok-building-games/references/genres/voxel-minecraft.md +0 -89
  157. package/skills/grok-building-games/references/input.md +0 -139
  158. package/skills/grok-building-games/references/phaser.md +0 -277
  159. package/skills/grok-building-games/references/procedural-generation.md +0 -141
  160. package/skills/grok-building-games/references/save-persistence.md +0 -108
  161. package/skills/grok-building-games/references/threejs-foundational.md +0 -148
  162. package/skills/grok-controls/SKILL.md +0 -274
  163. package/skills/grok-controls/agentsam.skill.json +0 -25
  164. package/skills/grok-design-ui/SKILL.md +0 -122
  165. package/skills/grok-design-ui/agentsam.skill.json +0 -25
  166. package/skills/grok-design-ui/references/animations.md +0 -379
  167. package/skills/grok-design-ui/references/performance.md +0 -88
  168. package/skills/grok-design-ui/references/refined-ui.md +0 -384
  169. package/skills/grok-design-ui/references/surfaces.md +0 -247
  170. package/skills/grok-design-ui/references/typography.md +0 -123
  171. package/skills/grok-game-animation-frames/SKILL.md +0 -84
  172. package/skills/grok-game-animation-frames/agentsam.skill.json +0 -25
  173. package/skills/grok-game-asset-core/SKILL.md +0 -69
  174. package/skills/grok-game-asset-core/agentsam.skill.json +0 -25
  175. package/skills/grok-game-character-consistency/SKILL.md +0 -69
  176. package/skills/grok-game-character-consistency/agentsam.skill.json +0 -25
  177. package/skills/grok-game-tilesets/SKILL.md +0 -65
  178. package/skills/grok-game-tilesets/agentsam.skill.json +0 -25
  179. package/skills/grok-game-ui-icons/SKILL.md +0 -56
  180. package/skills/grok-game-ui-icons/agentsam.skill.json +0 -25
  181. package/skills/grok-generate2dmap/LICENSE +0 -21
  182. package/skills/grok-generate2dmap/SKILL.md +0 -120
  183. package/skills/grok-generate2dmap/SOURCE.md +0 -6
  184. package/skills/grok-generate2dmap/agentsam.skill.json +0 -25
  185. package/skills/grok-generate2dmap/references/deliverables.md +0 -101
  186. package/skills/grok-generate2dmap/references/layered-map-contract.md +0 -209
  187. package/skills/grok-generate2dmap/references/map-strategies.md +0 -341
  188. package/skills/grok-generate2dmap/references/object-production-gate.md +0 -24
  189. package/skills/grok-generate2dmap/references/pipeline.md +0 -149
  190. package/skills/grok-generate2dmap/references/prop-pack-contract.md +0 -174
  191. package/skills/grok-generate2dmap/references/side-scroll-stages.md +0 -29
  192. package/skills/grok-generate2dmap/scripts/compose_layered_preview.py +0 -157
  193. package/skills/grok-generate2dmap/scripts/extract_prop_pack.py +0 -369
  194. package/skills/grok-generate2dsprite/LICENSE +0 -21
  195. package/skills/grok-generate2dsprite/SKILL.md +0 -313
  196. package/skills/grok-generate2dsprite/SOURCE.md +0 -6
  197. package/skills/grok-generate2dsprite/agentsam.skill.json +0 -25
  198. package/skills/grok-generate2dsprite/references/modes.md +0 -124
  199. package/skills/grok-generate2dsprite/references/prompt-rules.md +0 -315
  200. package/skills/grok-generate2dsprite/scripts/generate2dsprite.py +0 -1031
  201. package/skills/grok-generate2dsprite/scripts/make_layout_guide.py +0 -109
  202. package/skills/grok-imagine/SKILL.md +0 -201
  203. package/skills/grok-imagine/agentsam.skill.json +0 -25
  204. package/skills/grok-multiplayer-p2p/SKILL.md +0 -111
  205. package/skills/grok-multiplayer-p2p/agentsam.skill.json +0 -25
  206. package/skills/grok-multiplayer-p2p/references/react-binding.md +0 -144
  207. package/skills/grok-multiplayer-p2p/references/signaling-relay.md +0 -269
  208. package/skills/grok-neon/SKILL.md +0 -157
  209. package/skills/grok-neon/agentsam.skill.json +0 -25
  210. package/skills/grok-og/SKILL.md +0 -102
  211. package/skills/grok-og/agentsam.skill.json +0 -25
  212. package/skills/grok-og/references/brand-pass.md +0 -53
  213. package/skills/grok-og/references/custom-card.md +0 -107
  214. package/skills/grok-og/references/favicon-and-icons.md +0 -48
  215. package/skills/grok-og/references/og-type-contract.md +0 -18
  216. package/skills/grok-og/references/placeholder-card.md +0 -21
  217. package/skills/grok-og/references/x-banner.md +0 -41
  218. package/skills/grok-threejs/SKILL.md +0 -67
  219. package/skills/grok-threejs/agentsam.skill.json +0 -25
  220. package/skills/grok-threejs/references/llms-full.txt +0 -2780
  221. package/skills/grok-video2dsprite/LICENSE +0 -21
  222. package/skills/grok-video2dsprite/SKILL.md +0 -236
  223. package/skills/grok-video2dsprite/SOURCE.md +0 -6
  224. package/skills/grok-video2dsprite/agentsam.skill.json +0 -25
  225. package/skills/grok-video2dsprite/references/pipeline.md +0 -90
  226. package/skills/grok-video2dsprite/references/prompt-rules.md +0 -63
  227. package/skills/grok-video2dsprite/scripts/video2dsprite.py +0 -490
  228. package/skills/grok-xai-api/SKILL.md +0 -152
  229. package/skills/grok-xai-api/agentsam.skill.json +0 -25
  230. package/test/account-session.test.mjs +0 -101
  231. package/test/agent-capabilities.test.mjs +0 -67
  232. package/test/app-authority-context.test.mjs +0 -32
  233. package/test/app-building-skills.test.mjs +0 -61
  234. package/test/apps-scaffold-contract.test.mjs +0 -115
  235. package/test/blender-cad.test.mjs +0 -146
  236. package/test/cad-docker.test.mjs +0 -96
  237. package/test/capabilities.test.mjs +0 -133
  238. package/test/cli/api-key-env-whoami.test.mjs +0 -129
  239. package/test/cli/catalog-authority.test.mjs +0 -64
  240. package/test/cli/codebaseindex-plan-ux.test.mjs +0 -37
  241. package/test/cli/eval-live.test.mjs +0 -219
  242. package/test/cli/go.test.mjs +0 -128
  243. package/test/cli/local-vault-and-provider-setup.test.mjs +0 -268
  244. package/test/cli/machine-command.test.mjs +0 -34
  245. package/test/cli/mcp-client.test.mjs +0 -256
  246. package/test/cli/preferences-runtime.test.mjs +0 -11
  247. package/test/cli/runtime-persist.test.mjs +0 -56
  248. package/test/cli/runtime-ui.test.mjs +0 -141
  249. package/test/cli/rust-command.test.mjs +0 -21
  250. package/test/cli/skill-npm-and-env.test.mjs +0 -52
  251. package/test/cli/status-actions.test.mjs +0 -88
  252. package/test/cli/wireframes-go-registry.test.mjs +0 -139
  253. package/test/cli-preferences.test.mjs +0 -46
  254. package/test/cloudflare-connector.test.mjs +0 -93
  255. package/test/cloudflare-runtime.test.mjs +0 -82
  256. package/test/context.test.mjs +0 -159
  257. package/test/credentials-audit.test.mjs +0 -113
  258. package/test/deploy-health-scan.test.mjs +0 -95
  259. package/test/deploy-receipt.test.mjs +0 -91
  260. package/test/dockerize.test.mjs +0 -93
  261. package/test/error-diagnostics.test.mjs +0 -124
  262. package/test/eval-context.test.mjs +0 -37
  263. package/test/execution-approvals.test.mjs +0 -27
  264. package/test/fixtures/cad/courtyard-house.json +0 -208
  265. package/test/fixtures/knowledge-rpc-worker.mjs +0 -16
  266. package/test/fixtures/project-authority/customer-app/.agentsam/app.json +0 -5
  267. package/test/fixtures/project-authority/customer-app/apps/my-product/index.ts +0 -1
  268. package/test/fixtures/project-authority/customer-app/wrangler.toml +0 -17
  269. package/test/gcloud-auth.test.mjs +0 -111
  270. package/test/go/build-source-identity.test.mjs +0 -31
  271. package/test/go/cloudflare-probe.test.mjs +0 -317
  272. package/test/go/distribution.test.mjs +0 -28
  273. package/test/google-cloud-connection.test.mjs +0 -83
  274. package/test/google-cloud-inventory.test.mjs +0 -64
  275. package/test/indexing-provider.test.mjs +0 -29
  276. package/test/init-options.test.mjs +0 -35
  277. package/test/integration/ai-access-onboarding.test.mjs +0 -47
  278. package/test/integration/app-command.test.mjs +0 -85
  279. package/test/integration/autorag-project-authority.integration.test.mjs +0 -51
  280. package/test/integration/autorag-runtime-discovery.test.mjs +0 -107
  281. package/test/integration/cad-discovery.test.mjs +0 -158
  282. package/test/integration/cad-docker-executor.test.mjs +0 -249
  283. package/test/integration/cad-execution.test.mjs +0 -184
  284. package/test/integration/cad-project-app.test.mjs +0 -31
  285. package/test/integration/cad-project-d1.test.mjs +0 -25
  286. package/test/integration/cad-project-native.test.mjs +0 -24
  287. package/test/integration/cad-project-workflow.test.mjs +0 -9
  288. package/test/integration/cad-project.test.mjs +0 -85
  289. package/test/integration/cli-help.test.mjs +0 -41
  290. package/test/integration/cms-app-scaffold.test.mjs +0 -77
  291. package/test/integration/cms-site-tenancy-contract.test.mjs +0 -268
  292. package/test/integration/codebaseindex-ingest.test.mjs +0 -255
  293. package/test/integration/core-client.integration.test.mjs +0 -55
  294. package/test/integration/database-hosted-honesty.test.mjs +0 -49
  295. package/test/integration/desktop-keychain-boundary.test.mjs +0 -45
  296. package/test/integration/error-runtime-rpc.test.mjs +0 -71
  297. package/test/integration/features-resolve.test.mjs +0 -147
  298. package/test/integration/hosted-work-host-authority.test.mjs +0 -47
  299. package/test/integration/icon-registry.test.mjs +0 -67
  300. package/test/integration/ingest-discover-models.test.mjs +0 -30
  301. package/test/integration/inspect-path-cli.test.mjs +0 -46
  302. package/test/integration/install-script.test.mjs +0 -110
  303. package/test/integration/inventory-core.test.mjs +0 -53
  304. package/test/integration/inventory-eligibility.test.mjs +0 -59
  305. package/test/integration/inventory-evidence.test.mjs +0 -125
  306. package/test/integration/knowledge-rpc.test.mjs +0 -112
  307. package/test/integration/local-fs.test.mjs +0 -113
  308. package/test/integration/local-provider-bridge.test.mjs +0 -65
  309. package/test/integration/local-studio-settings-production.test.mjs +0 -40
  310. package/test/integration/lockfile-sync.test.mjs +0 -26
  311. package/test/integration/merkle-cli.test.mjs +0 -61
  312. package/test/integration/merkle-persistence-identity.test.mjs +0 -48
  313. package/test/integration/plugins-cloudflare.integration.test.mjs +0 -28
  314. package/test/integration/provider-env-cli.test.mjs +0 -60
  315. package/test/integration/provider-factory.test.mjs +0 -197
  316. package/test/integration/repository-company-graph.test.mjs +0 -90
  317. package/test/integration/runtime-migrations.test.mjs +0 -87
  318. package/test/integration/runtime-status.integration.test.mjs +0 -227
  319. package/test/integration/sam-activity-recovery.test.mjs +0 -147
  320. package/test/integration/sam-decision.test.mjs +0 -584
  321. package/test/integration/sam-kernel.test.mjs +0 -99
  322. package/test/integration/sam-planning-astar.test.mjs +0 -279
  323. package/test/integration/scaffold-cloudflare.test.mjs +0 -46
  324. package/test/integration/skill-runtime.test.mjs +0 -240
  325. package/test/integration/storage-architecture.test.mjs +0 -154
  326. package/test/integration/store-discovery.test.mjs +0 -44
  327. package/test/integration/studio-fs-pty-e2e.test.mjs +0 -294
  328. package/test/integration/terminal-exec.integration.test.mjs +0 -86
  329. package/test/integration/theme-package-registry.test.mjs +0 -31
  330. package/test/integration/tool-runtime.test.mjs +0 -124
  331. package/test/integration/tool-schema.test.mjs +0 -59
  332. package/test/jr-dev-skill.test.mjs +0 -26
  333. package/test/knowledge-context-pack.test.mjs +0 -19
  334. package/test/knowledge-postgres.test.mjs +0 -40
  335. package/test/knowledge-service.test.mjs +0 -110
  336. package/test/knowledge.test.mjs +0 -206
  337. package/test/live/terminal-transport.live.test.mjs +0 -24
  338. package/test/local-sessions.test.mjs +0 -57
  339. package/test/local-studio-deploy.test.mjs +0 -83
  340. package/test/loopback-oauth.test.mjs +0 -53
  341. package/test/merkle-render.test.mjs +0 -31
  342. package/test/mini.test.mjs +0 -140
  343. package/test/model-catalog.test.mjs +0 -70
  344. package/test/models.test.mjs +0 -309
  345. package/test/npm10-lock.test.mjs +0 -29
  346. package/test/ollama.test.mjs +0 -137
  347. package/test/open-url.test.mjs +0 -64
  348. package/test/openai-responses.test.mjs +0 -155
  349. package/test/portable-context.test.mjs +0 -94
  350. package/test/project-config.test.mjs +0 -81
  351. package/test/project-rules.test.mjs +0 -44
  352. package/test/provider-credentials.test.mjs +0 -106
  353. package/test/rehydrate.test.mjs +0 -25
  354. package/test/release-hygiene.test.mjs +0 -42
  355. package/test/repository-snapshot-view.test.mjs +0 -112
  356. package/test/responses-runner.test.mjs +0 -465
  357. package/test/runtime-activity.test.mjs +0 -98
  358. package/test/sdk-worker-contract.test.mjs +0 -72
  359. package/test/security.test.mjs +0 -266
  360. package/test/setup-planner.test.mjs +0 -74
  361. package/test/setup-runtime.test.mjs +0 -51
  362. package/test/shell.test.mjs +0 -281
  363. package/test/skills.test.mjs +0 -22
  364. package/test/smoke.mjs +0 -122
  365. package/test/telemetry.test.mjs +0 -79
  366. package/test/terminal/local-pty.mock.test.mjs +0 -153
  367. package/test/terminal-machine-identity.test.mjs +0 -88
  368. package/test/theme-portability.test.mjs +0 -14
  369. package/test/tools-search.test.mjs +0 -73
  370. package/test/unit/website-assets-bindings.test.mjs +0 -60
  371. package/test/whoami-resume.test.mjs +0 -157
@@ -1,1031 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Build sprite prompts and postprocess generated sprite sheets locally."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import hashlib
8
- import json
9
- import math
10
- import random
11
- import re
12
- from collections import deque
13
- from pathlib import Path
14
-
15
- import numpy as np
16
- from PIL import Image
17
-
18
- ART_STYLE = (
19
- "Original digital monster creature. Digimon/Pokemon inspired pixel art, "
20
- "strong outlines, dynamic, battle-ready. NOT cute, NOT round. "
21
- "SOLID COLORED BODY. Background is 100% solid flat magenta (#FF00FF), no gradients. "
22
- "NO text, NO labels, NO words, NO letters anywhere."
23
- )
24
-
25
- CHAR_STYLE = (
26
- "Top-down 2D pixel art for a 16-bit RPG overworld. 3/4 view from slightly "
27
- "above, you can see the top of the head, shoulders and full body. Chunky "
28
- "pixel-art with crisp dark outlines and saturated colors. Character fills "
29
- "~60% of its cell with margin for the engine to render cleanly. "
30
- "Background is 100% solid flat magenta (#FF00FF), no gradients, no shadow "
31
- "under character. NO text, NO labels, NO UI, NO speech bubbles."
32
- )
33
-
34
- GRID_RULES = (
35
- "ABSOLUTE RULES: "
36
- "1. EXACTLY 4 equal quadrants (2x2). "
37
- "2. NO borders, NO lines, NO frames between quadrants. "
38
- "3. NO text, NO labels. "
39
- "4. Each character fills 80%+ of its quadrant, SAME SIZE in every quadrant. "
40
- "5. Quadrants connected by magenta background only."
41
- )
42
-
43
- GRID_RULES_4X4 = (
44
- "ABSOLUTE RULES: "
45
- "1. EXACTLY 16 equal-size cells arranged in a 4x4 grid (4 rows of 4 columns, every cell the same width and height). "
46
- "2. NO borders, NO lines, NO frames between cells. "
47
- "3. NO text, NO labels, NO numbers, NO arrows. "
48
- "4. CRITICAL CONSISTENCY: the character in every single cell has the IDENTICAL height and IDENTICAL width "
49
- "(same bounding box, same pixel scale). Do NOT zoom in or out between cells. "
50
- "Do NOT crop tighter in some cells. The character's head-to-foot height must be visibly identical in all 16 cells. "
51
- "5. Character is CENTERED horizontally and vertically within its cell. Fills ~60% of the cell, leaving equal magenta margin on all four sides. "
52
- "6. Cells connected ONLY by solid magenta (#FF00FF) background."
53
- )
54
-
55
- NPC_ROLES = {
56
- "starter": "an experienced mentor who hands out starter monsters, wise and welcoming",
57
- "shop": "a merchant or vendor, apron or utility belt, counter accessories",
58
- "healer": "a healer or medic, soft uniform, healing tools, calm posture",
59
- "summoner": "a mystical summoner who calls forth monsters, arcane or gambling vibe",
60
- "sage": "an old wise sage, robes or long coat, staff or crystal",
61
- "trainer": "a rival trainer, confident athletic pose, slight smirk",
62
- "gym_leader": "a gym leader or boss, distinct outfit, most powerful regional trainer",
63
- "villager": "an ordinary townsperson, plain outfit, friendly body language",
64
- "guard": "a city guard, uniform or armor, alert posture",
65
- }
66
-
67
- GENERIC_ASSET_MODES = [
68
- "single",
69
- "idle",
70
- "cast",
71
- "attack",
72
- "shoot",
73
- "jump",
74
- "hurt",
75
- "combat",
76
- "walk",
77
- "run",
78
- "hover",
79
- "charge",
80
- "projectile",
81
- "impact",
82
- "explode",
83
- "death",
84
- "fx",
85
- "sheet",
86
- ]
87
-
88
- # Modes that are single-frame (not a multi-cell grid) when --rows/--cols omitted.
89
- SINGLE_MODES = frozenset({"single", "player", "npc"})
90
-
91
- TARGET_MODES = {
92
- "creature": ["single", "evolution", "idle", "combat", "walk", "actions"],
93
- "player": [
94
- "player",
95
- "player_walk",
96
- "player_sheet",
97
- "player_actions",
98
- "idle",
99
- "walk",
100
- "run",
101
- "attack",
102
- "shoot",
103
- "jump",
104
- "hurt",
105
- "cast",
106
- ],
107
- "npc": ["npc", "npc_walk", "idle", "walk"],
108
- "asset": GENERIC_ASSET_MODES,
109
- }
110
-
111
- GRID_SHAPES = {
112
- "evolution": (2, 2),
113
- "idle": (2, 2),
114
- "cast": (2, 3),
115
- "attack": (2, 2),
116
- "shoot": (2, 2),
117
- "jump": (2, 2),
118
- "hurt": (2, 2),
119
- "combat": (2, 2),
120
- "actions": (2, 2),
121
- "walk": (2, 2),
122
- "run": (2, 2),
123
- "hover": (2, 2),
124
- "charge": (2, 2),
125
- "projectile": (2, 2), # multi-row default for body-attached loops; override with --rows/--cols
126
- "impact": (2, 2),
127
- "explode": (2, 2),
128
- "death": (2, 3),
129
- "fx": (2, 2),
130
- "sheet": (2, 2),
131
- "player_walk": (2, 2),
132
- "player_actions": (2, 2),
133
- "npc_walk": (2, 2),
134
- "player_sheet": (4, 4),
135
- }
136
-
137
- FRAME_LABELS = {
138
- "evolution": ["stage-1", "stage-2", "stage-3", "stage-4"],
139
- "idle": ["idle-1", "idle-2", "idle-3", "idle-4"],
140
- "cast": ["cast-1", "cast-2", "cast-3", "cast-4", "cast-5", "cast-6"],
141
- "attack": ["attack-1", "attack-2", "attack-3", "attack-4"],
142
- "shoot": ["shoot-1", "shoot-2", "shoot-3", "shoot-4"],
143
- "jump": ["jump-1", "jump-2", "jump-3", "jump-4"],
144
- "hurt": ["hurt-1", "hurt-2", "hurt-3", "hurt-4"],
145
- "combat": ["attack-1", "attack-2", "hurt-1", "hurt-2"],
146
- "actions": ["idle-1", "idle-2", "attack", "hurt"],
147
- "walk": ["walk-1", "walk-2", "walk-3", "walk-4"],
148
- "run": ["run-1", "run-2", "run-3", "run-4"],
149
- "hover": ["hover-1", "hover-2", "hover-3", "hover-4"],
150
- "charge": ["charge-1", "charge-2", "charge-3", "charge-4"],
151
- "projectile": ["projectile-1", "projectile-2", "projectile-3", "projectile-4"],
152
- "impact": ["impact-1", "impact-2", "impact-3", "impact-4"],
153
- "explode": ["explode-1", "explode-2", "explode-3", "explode-4"],
154
- "death": ["death-1", "death-2", "death-3", "death-4", "death-5", "death-6"],
155
- "fx": ["fx-1", "fx-2", "fx-3", "fx-4"],
156
- "sheet": ["frame-1", "frame-2", "frame-3", "frame-4"],
157
- "player_walk": ["walk-down-1", "walk-down-2", "walk-down-3", "walk-down-4"],
158
- "player_actions": ["idle", "walk", "attack", "hurt"],
159
- "npc_walk": ["walk-down-1", "walk-down-2", "walk-down-3", "walk-down-4"],
160
- "player_sheet": [
161
- "down-1",
162
- "down-2",
163
- "down-3",
164
- "down-4",
165
- "left-1",
166
- "left-2",
167
- "left-3",
168
- "left-4",
169
- "right-1",
170
- "right-2",
171
- "right-3",
172
- "right-4",
173
- "up-1",
174
- "up-2",
175
- "up-3",
176
- "up-4",
177
- ],
178
- }
179
-
180
- PROCESS_TARGETS = sorted(TARGET_MODES)
181
-
182
- ARCHETYPES = {
183
- "beast": {
184
- "name": "Beast Evolution",
185
- "path": "primal beast to apex predator to mythic god-beast",
186
- },
187
- "mecha": {"name": "Mecha Evolution", "path": "organic to cybernetic to full mecha to mech-god"},
188
- "elemental": {
189
- "name": "Elemental Evolution",
190
- "path": "solid creature to elemental infused to pure energy being",
191
- },
192
- "void": {
193
- "name": "Void Evolution",
194
- "path": "shadow creature to twisted horror to abstract cosmic entity",
195
- },
196
- "crystal": {
197
- "name": "Crystal Evolution",
198
- "path": "rocky creature to crystalline to geometric god",
199
- },
200
- "angelic": {"name": "Angelic Evolution", "path": "creature to holy warrior to divine seraph"},
201
- "parasite": {
202
- "name": "Parasite Evolution",
203
- "path": "small symbiote to merged chimera to eldritch abomination",
204
- },
205
- "myth": {"name": "Myth Evolution", "path": "animal to mythical beast to ancient deity"},
206
- }
207
-
208
- MORPH_AXES = {
209
- "posture": ["quadrupedal", "bipedal", "floating", "abstract or formless"],
210
- "material": ["flesh or organic", "armored or plated", "energy-infused", "pure light or energy"],
211
- "anatomy": [
212
- "compact limbs",
213
- "extended limbs plus tail",
214
- "extra limbs or wings",
215
- "aura replaces body parts",
216
- ],
217
- }
218
-
219
- SILHOUETTES = ["sharp angular", "bulky imposing", "elongated serpentine", "alien geometric"]
220
- SURFACES = ["smooth organic", "armored plates", "crystalline facets", "energy veins"]
221
- VIBES = ["elegant and swift", "brutal and heavy", "mysterious and dark", "sacred and divine"]
222
-
223
-
224
- def stable_seed(target: str, mode: str, prompt: str, role: str) -> int:
225
- raw = f"{target}|{mode}|{prompt}|{role}".encode("utf-8")
226
- return int(hashlib.sha256(raw).hexdigest()[:8], 16)
227
-
228
-
229
- def is_known_target_mode(target: str, mode: str) -> bool:
230
- return target in TARGET_MODES and mode in TARGET_MODES[target]
231
-
232
-
233
- def ensure_valid_target_mode(target: str, mode: str) -> None:
234
- if target not in TARGET_MODES:
235
- raise ValueError(
236
- f"Unknown target '{target}'. Valid targets: {', '.join(sorted(TARGET_MODES))}"
237
- )
238
- if mode not in TARGET_MODES[target]:
239
- allowed = ", ".join(TARGET_MODES[target])
240
- raise ValueError(f"Mode '{mode}' is invalid for target '{target}'. Valid modes: {allowed}")
241
-
242
-
243
- def build_evolution_descs(subject: str, rng: random.Random) -> dict[str, str]:
244
- arch_key = rng.choice(list(ARCHETYPES.keys()))
245
- arch = ARCHETYPES[arch_key]
246
- silhouette = rng.choice(SILHOUETTES)
247
- surface = rng.choice(SURFACES)
248
- vibe = rng.choice(VIBES)
249
- postures = MORPH_AXES["posture"]
250
- materials = MORPH_AXES["material"]
251
- anatomies = MORPH_AXES["anatomy"]
252
-
253
- design_rules = (
254
- f"Evolution archetype: {arch['name']} ({arch['path']}). "
255
- f"Design: {silhouette} silhouette, {surface} surface, {vibe} feel. "
256
- "Ensure DIFFERENT silhouette, texture, posture per stage. "
257
- "Avoid repeating limb structure or proportions."
258
- )
259
-
260
- return {
261
- "1-base": (
262
- f"Stage 1: Base form. The clearest first complete form of {subject}. "
263
- f"Posture: {postures[0]}. Material: {materials[0]}. Anatomy: {anatomies[0]}. "
264
- f"Strong identity and readable silhouette. {design_rules}"
265
- ),
266
- "2-risen": (
267
- f"Stage 2: Developed form. A more dangerous promoted version of {subject}. "
268
- f"Posture: {postures[1]} and clearly different from stage 1. "
269
- f"Material: {materials[1]}. Anatomy: {anatomies[1]}. "
270
- f"REDESIGNED, not just bigger. Combat specialist. {design_rules}"
271
- ),
272
- "3-elite": (
273
- f"Stage 3: Elite war form of {subject}. "
274
- f"Posture: {postures[2]}. Material: {materials[2]}. Anatomy: {anatomies[2]}. "
275
- f"Heavy battlefield presence and advanced redesign. {design_rules}"
276
- ),
277
- "4-mythic": (
278
- f"Stage 4: Mythic ascendant form of {subject}. "
279
- f"Posture: {postures[3]}. Material: {materials[3]}. Anatomy: {anatomies[3]}. "
280
- f"Abstract, cosmic, godlike final evolution. {design_rules}"
281
- ),
282
- }
283
-
284
-
285
- def build_prompt(
286
- target: str, mode: str, prompt: str, role: str | None = None, seed: int | None = None
287
- ) -> tuple[str, int]:
288
- ensure_valid_target_mode(target, mode)
289
- role = role or ""
290
- if seed is None:
291
- seed = stable_seed(target, mode, prompt, role)
292
- rng = random.Random(seed)
293
-
294
- if target == "creature":
295
- if mode == "single":
296
- result = (
297
- f"Single pixel art creature sprite, centered, facing right. {prompt}. {ART_STYLE}"
298
- )
299
- elif mode == "evolution":
300
- descs = build_evolution_descs(prompt, rng)
301
- result = (
302
- f"A 2x2 pixel art image showing 4 evolution stages of {prompt}. "
303
- f"Top-left quadrant: {descs['1-base']} "
304
- f"Top-right quadrant: {descs['2-risen']} "
305
- f"Bottom-left quadrant: {descs['3-elite']} "
306
- f"Bottom-right quadrant: {descs['4-mythic']} "
307
- f"Same color palette in all 4. {ART_STYLE} {GRID_RULES}"
308
- )
309
- elif mode == "idle":
310
- result = (
311
- f"A 2x2 pixel art idle animation sheet of the same {prompt}. "
312
- "Top-left quadrant: neutral idle pose, calm but alert. "
313
- "Top-right quadrant: subtle breath or flame pulse, same facing direction. "
314
- "Bottom-left quadrant: idle shift in weight or aura, still clearly looping. "
315
- "Bottom-right quadrant: strongest idle accent before returning to frame 1. "
316
- f"SAME creature, SAME size, SAME facing direction, SAME palette in all 4 cells. {ART_STYLE} {GRID_RULES}"
317
- )
318
- elif mode == "combat":
319
- result = (
320
- f"A 2x2 pixel art combat sheet of the same {prompt}. "
321
- "Top-left quadrant: attack wind-up, gathering force. "
322
- "Top-right quadrant: attack strike or release, aggressive impact. "
323
- "Bottom-left quadrant: hurt reaction at the moment of impact. "
324
- "Bottom-right quadrant: hurt recovery, regaining stance. "
325
- f"SAME creature, SAME size, SAME facing direction, SAME palette in all 4 cells. {ART_STYLE} {GRID_RULES}"
326
- )
327
- elif mode == "actions":
328
- result = (
329
- f"A 2x2 pixel art sprite sheet of the same {prompt} in 4 poses. "
330
- "Top-left quadrant: standing still, relaxed. "
331
- "Top-right quadrant: same pose, mouth open, one limb lifted. "
332
- "Bottom-left quadrant: lunging right, attacking fiercely. "
333
- "Bottom-right quadrant: leaning back, eyes closed, taking damage. "
334
- f"SAME character, SAME size, facing RIGHT. {ART_STYLE} {GRID_RULES}"
335
- )
336
- else:
337
- result = (
338
- f"A 2x2 pixel art sprite sheet of a walk cycle of the same {prompt}. "
339
- "Top-left quadrant: walking right, right front leg forward. "
340
- "Top-right quadrant: walking right, legs under body, mid-stride. "
341
- "Bottom-left quadrant: walking right, left front leg forward. "
342
- "Bottom-right quadrant: walking right, legs extended, passing pose. "
343
- f"SAME character, SAME size, facing RIGHT. Only leg positions change. {ART_STYLE} {GRID_RULES}"
344
- )
345
- elif target == "player":
346
- if mode == "player":
347
- result = (
348
- "Single hero sprite for a top-down RPG. "
349
- f"CHARACTER: {prompt}. Young adventurer protagonist, distinct heroic silhouette, strongly themed costume. "
350
- "Front-facing (toward camera), idle standing pose, centered in canvas with lots of magenta margin around. "
351
- f"{CHAR_STYLE}"
352
- )
353
- elif mode in {"player_walk", "walk", "run"}:
354
- action = "run" if mode == "run" else "walk"
355
- result = (
356
- f"A 2x2 pixel art sprite sheet of a top-down RPG hero {action} cycle, ALL FRAMES FACING DOWN (toward camera). "
357
- f"CHARACTER: {prompt}. "
358
- "Top-left: neutral standing, both feet together. "
359
- "Top-right: LEFT foot stepping forward, right foot planted. "
360
- "Bottom-left: neutral standing again, both feet together. "
361
- "Bottom-right: RIGHT foot stepping forward, left foot planted. "
362
- "SAME character, SAME costume, SAME palette in every cell. "
363
- f"ONLY the legs and arms swing, head, torso, gear stay identical. {CHAR_STYLE} {GRID_RULES}"
364
- )
365
- elif mode == "player_sheet":
366
- result = (
367
- "A 4x4 pixel art sprite sheet, full 4-direction walk cycle for a top-down RPG hero. "
368
- f"CHARACTER: {prompt}. Young adventurer protagonist. "
369
- "SHEET LAYOUT (rows = facing direction, columns = walk frames): "
370
- "Row 1 (top): facing DOWN (toward camera, face fully visible). "
371
- "Row 2: facing LEFT (left profile or side view). "
372
- "Row 3: facing RIGHT (right profile or side view, mirror of row 2). "
373
- "Row 4 (bottom): facing UP (away from camera, back of head visible). "
374
- "COLUMN 1: neutral pose, both feet together. "
375
- "COLUMN 2: LEFT foot stepping forward. "
376
- "COLUMN 3: neutral pose again, both feet together. "
377
- "COLUMN 4: RIGHT foot stepping forward. "
378
- "IDENTICAL SIZE in every cell: same character height head-to-foot, same width shoulder-to-shoulder, "
379
- "same on-screen pixel scale. No zooming, no cropping differently, only pose and direction change. "
380
- "SAME character identity, SAME costume, SAME palette in all 16 cells. "
381
- "The head and torso orientation must clearly communicate which direction the character is facing in each row. "
382
- f"{CHAR_STYLE} {GRID_RULES_4X4}"
383
- )
384
- elif mode in {"player_actions", "actions"}:
385
- result = (
386
- "A 2x2 pixel art sprite sheet of a top-down RPG hero in 4 action states, all facing DOWN (toward camera). "
387
- f"CHARACTER: {prompt}. "
388
- "Top-left: IDLE, neutral standing, relaxed. "
389
- "Top-right: WALK, mid-step, one leg forward. "
390
- "Bottom-left: ATTACK, arm raised or weapon or fist swung forward aggressively. "
391
- "Bottom-right: HURT, knocked back slightly, expression of pain. "
392
- f"SAME character identity, SAME costume, SAME size in every cell. {CHAR_STYLE} {GRID_RULES}"
393
- )
394
- else:
395
- # Per-action hero sheets (idle/attack/shoot/jump/cast/hurt/…) — one
396
- # coherent action grid, matching GRID_SHAPES / FRAME_LABELS used by process.
397
- rows, cols = GRID_SHAPES.get(mode, (2, 2))
398
- result = (
399
- f"A {rows}x{cols} pixel art animation sheet of a top-down RPG hero performing '{mode}'. "
400
- f"CHARACTER: {prompt}. "
401
- "Same character identity, costume, palette, and body scale in every cell. "
402
- "Body-only unless the mode is intentionally FX/projectile/impact. "
403
- "Centered in each cell with solid magenta (#FF00FF) margins; nothing crosses cell edges. "
404
- f"{CHAR_STYLE}"
405
- )
406
- elif target == "npc":
407
- if role not in NPC_ROLES:
408
- allowed = ", ".join(sorted(NPC_ROLES))
409
- raise ValueError(f"NPC role is required for target=npc. Valid roles: {allowed}")
410
- role_desc = NPC_ROLES[role]
411
- if mode == "npc":
412
- result = (
413
- "Single NPC sprite for a top-down RPG. "
414
- f"ROLE: {role_desc}. "
415
- f"VISUAL DETAILS: {prompt}. "
416
- "Front-facing (toward camera), idle standing pose. "
417
- "Appearance should INSTANTLY communicate the role. "
418
- f"Distinct silhouette and palette so this NPC won't be confused with others. {CHAR_STYLE}"
419
- )
420
- elif mode in {"npc_walk", "walk"}:
421
- result = (
422
- "A 2x2 pixel art sprite sheet, top-down RPG NPC walk cycle, ALL FRAMES FACING DOWN (toward camera). "
423
- f"ROLE: {role_desc}. "
424
- f"VISUAL DETAILS: {prompt}. "
425
- "Top-left: neutral standing, both feet together. "
426
- "Top-right: LEFT foot stepping forward. "
427
- "Bottom-left: neutral standing again. "
428
- "Bottom-right: RIGHT foot stepping forward. "
429
- f"SAME NPC, SAME costume, SAME palette in every cell. {CHAR_STYLE} {GRID_RULES}"
430
- )
431
- else:
432
- rows, cols = GRID_SHAPES.get(mode, (2, 2))
433
- result = (
434
- f"A {rows}x{cols} pixel art animation sheet of a top-down RPG NPC performing '{mode}'. "
435
- f"ROLE: {role_desc}. "
436
- f"VISUAL DETAILS: {prompt}. "
437
- "Same NPC identity, costume, palette, and body scale in every cell. "
438
- "Centered in each cell with solid magenta (#FF00FF) margins; nothing crosses cell edges. "
439
- f"{CHAR_STYLE}"
440
- )
441
- else:
442
- if mode == "single":
443
- result = (
444
- f"Single pixel art asset sprite. SUBJECT: {prompt}. "
445
- "Centered in the canvas with clear magenta margin around it. "
446
- "Readable silhouette, game-ready shape consistency, transparent-background-ready via magenta chroma key. "
447
- f"{ART_STYLE}"
448
- )
449
- else:
450
- rows, cols = GRID_SHAPES.get(mode, (2, 2))
451
- result = (
452
- f"A {rows}x{cols} pixel art animation sheet of the same {prompt}. "
453
- "The same asset identity appears in every cell, with the same bounding box, the same pixel scale, "
454
- "and no part crossing a cell edge. "
455
- "Keep the animation readable for a 2D game sprite, not a splash illustration. "
456
- f"{ART_STYLE}"
457
- )
458
- return result, seed
459
-
460
-
461
- def remove_bg_magenta(
462
- img: Image.Image, threshold: int = 100, edge_threshold: int = 150
463
- ) -> Image.Image:
464
- pixels = img.load()
465
- width, height = img.size
466
-
467
- def dist(r: int, g: int, b: int) -> float:
468
- return math.sqrt((r - 255) ** 2 + g**2 + (b - 255) ** 2)
469
-
470
- for x in range(width):
471
- for y in range(height):
472
- r, g, b, a = pixels[x, y]
473
- if a == 0:
474
- continue
475
- if dist(r, g, b) < threshold:
476
- pixels[x, y] = (0, 0, 0, 0)
477
-
478
- visited: set[tuple[int, int]] = set()
479
- queue: deque[tuple[int, int]] = deque()
480
- for x in range(width):
481
- queue.append((x, 0))
482
- queue.append((x, height - 1))
483
- for y in range(height):
484
- queue.append((0, y))
485
- queue.append((width - 1, y))
486
-
487
- while queue:
488
- x, y = queue.popleft()
489
- if (x, y) in visited or x < 0 or x >= width or y < 0 or y >= height:
490
- continue
491
- visited.add((x, y))
492
- r, g, b, a = pixels[x, y]
493
- if a == 0:
494
- for dx in (-1, 0, 1):
495
- for dy in (-1, 0, 1):
496
- if dx == 0 and dy == 0:
497
- continue
498
- if (x + dx, y + dy) not in visited:
499
- queue.append((x + dx, y + dy))
500
- elif dist(r, g, b) < edge_threshold:
501
- pixels[x, y] = (0, 0, 0, 0)
502
- for dx in (-1, 0, 1):
503
- for dy in (-1, 0, 1):
504
- if dx == 0 and dy == 0:
505
- continue
506
- if (x + dx, y + dy) not in visited:
507
- queue.append((x + dx, y + dy))
508
- return img
509
-
510
-
511
- def trim_border(img: Image.Image, px: int = 4) -> Image.Image:
512
- width, height = img.size
513
- if width > px * 2 and height > px * 2:
514
- return img.crop((px, px, width - px, height - px))
515
- return img
516
-
517
-
518
- def clean_edges(img: Image.Image, depth: int = 3) -> Image.Image:
519
- pixels = img.load()
520
- width, height = img.size
521
- for d in range(depth):
522
- for x in range(width):
523
- for y in (d, height - 1 - d):
524
- if y < 0 or y >= height:
525
- continue
526
- r, g, b, a = pixels[x, y]
527
- if a == 0:
528
- continue
529
- if (r < 40 and g < 40 and b < 40) or math.sqrt(
530
- (r - 255) ** 2 + g**2 + (b - 255) ** 2
531
- ) < 150:
532
- pixels[x, y] = (0, 0, 0, 0)
533
- for y in range(height):
534
- for x in (d, width - 1 - d):
535
- if x < 0 or x >= width:
536
- continue
537
- r, g, b, a = pixels[x, y]
538
- if a == 0:
539
- continue
540
- if (r < 40 and g < 40 and b < 40) or math.sqrt(
541
- (r - 255) ** 2 + g**2 + (b - 255) ** 2
542
- ) < 150:
543
- pixels[x, y] = (0, 0, 0, 0)
544
- return img
545
-
546
-
547
- def connected_components(img: Image.Image, min_area: int = 1) -> list[dict[str, object]]:
548
- alpha = img.getchannel("A")
549
- pixels = alpha.load()
550
- width, height = img.size
551
- visited = [[False] * width for _ in range(height)]
552
- components: list[dict[str, object]] = []
553
-
554
- for y in range(height):
555
- for x in range(width):
556
- if pixels[x, y] == 0 or visited[y][x]:
557
- continue
558
- queue: deque[tuple[int, int]] = deque([(x, y)])
559
- visited[y][x] = True
560
- area = 0
561
- min_x = max_x = x
562
- min_y = max_y = y
563
- touches_edge = x == 0 or y == 0 or x == width - 1 or y == height - 1
564
-
565
- while queue:
566
- cx, cy = queue.popleft()
567
- area += 1
568
- min_x = min(min_x, cx)
569
- min_y = min(min_y, cy)
570
- max_x = max(max_x, cx)
571
- max_y = max(max_y, cy)
572
- if cx == 0 or cy == 0 or cx == width - 1 or cy == height - 1:
573
- touches_edge = True
574
- for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
575
- nx, ny = cx + dx, cy + dy
576
- if (
577
- 0 <= nx < width
578
- and 0 <= ny < height
579
- and pixels[nx, ny] > 0
580
- and not visited[ny][nx]
581
- ):
582
- visited[ny][nx] = True
583
- queue.append((nx, ny))
584
-
585
- if area >= min_area:
586
- components.append(
587
- {
588
- "area": area,
589
- "bbox": (min_x, min_y, max_x + 1, max_y + 1),
590
- "touches_edge": touches_edge,
591
- }
592
- )
593
-
594
- components.sort(key=lambda item: int(item["area"]), reverse=True)
595
- return components
596
-
597
-
598
- def pad_bbox(
599
- bbox: tuple[int, int, int, int], padding: int, width: int, height: int
600
- ) -> tuple[int, int, int, int]:
601
- x0, y0, x1, y1 = bbox
602
- return (
603
- max(0, x0 - padding),
604
- max(0, y0 - padding),
605
- min(width, x1 + padding),
606
- min(height, y1 + padding),
607
- )
608
-
609
-
610
- def bbox_touches_edge(
611
- bbox: tuple[int, int, int, int] | None, width: int, height: int, margin: int = 0
612
- ) -> bool:
613
- if not bbox:
614
- return False
615
- x0, y0, x1, y1 = bbox
616
- return x0 <= margin or y0 <= margin or x1 >= width - margin or y1 >= height - margin
617
-
618
-
619
- def center_single_sprite(
620
- img: Image.Image, size: int, threshold: int, edge_threshold: int
621
- ) -> Image.Image:
622
- cleaned = remove_bg_magenta(img.convert("RGBA"), threshold, edge_threshold)
623
- bbox = cleaned.getbbox()
624
- if bbox:
625
- cleaned = cleaned.crop(bbox)
626
- width, height = cleaned.size
627
- canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
628
- if width > 0 and height > 0:
629
- scale = min(size / width, size / height) * 0.9
630
- new_width = max(1, int(width * scale))
631
- new_height = max(1, int(height * scale))
632
- cleaned = cleaned.resize((new_width, new_height), Image.Resampling.LANCZOS)
633
- canvas.paste(cleaned, ((size - new_width) // 2, (size - new_height) // 2))
634
- return canvas
635
-
636
-
637
- def split_grid(
638
- img: Image.Image,
639
- rows: int,
640
- cols: int,
641
- cell_size: int,
642
- threshold: int,
643
- edge_threshold: int,
644
- fit_scale: float = 0.85,
645
- trim_border_px: int = 4,
646
- edge_clean_depth: int = 3,
647
- align: str = "center",
648
- shared_scale: bool = False,
649
- component_mode: str = "all",
650
- component_padding: int = 0,
651
- min_component_area: int = 1,
652
- edge_touch_margin: int = 0,
653
- ) -> tuple[list[Image.Image], list[dict[str, object]]]:
654
- cleaned = remove_bg_magenta(img.convert("RGBA"), threshold, edge_threshold)
655
- width, height = cleaned.size
656
- cell_width, cell_height = width // cols, height // rows
657
- cropped_frames: list[Image.Image] = []
658
- frame_info: list[dict[str, object]] = []
659
- for row in range(rows):
660
- for col in range(cols):
661
- box = (
662
- col * cell_width,
663
- row * cell_height,
664
- (col + 1) * cell_width,
665
- (row + 1) * cell_height,
666
- )
667
- frame = cleaned.crop(box)
668
- # Edge-touch QC uses original grid-cell coords (pre-trim/pre-crop).
669
- edge_touch = bbox_touches_edge(
670
- frame.getbbox(), cell_width, cell_height, edge_touch_margin
671
- )
672
- if trim_border_px > 0:
673
- frame = trim_border(frame, px=trim_border_px)
674
- if edge_clean_depth > 0:
675
- frame = clean_edges(frame, depth=edge_clean_depth)
676
- components = connected_components(frame, min_area=min_component_area)
677
- bbox = None
678
- selected_component = None
679
- if component_mode == "largest" and components:
680
- selected_component = components[0]
681
- bbox = pad_bbox(
682
- tuple(selected_component["bbox"]), component_padding, frame.width, frame.height
683
- )
684
- else:
685
- bbox = frame.getbbox()
686
- if bbox:
687
- frame = frame.crop(bbox)
688
- cropped_frames.append(frame)
689
- frame_info.append(
690
- {
691
- "grid": [row, col],
692
- "source_box": list(box),
693
- "component_mode": component_mode,
694
- "component_count": len(components),
695
- "selected_component_area": int(selected_component["area"])
696
- if selected_component
697
- else None,
698
- "selected_component_bbox": list(selected_component["bbox"])
699
- if selected_component
700
- else None,
701
- "crop_bbox": list(bbox) if bbox else None,
702
- "edge_touch": edge_touch,
703
- }
704
- )
705
-
706
- common_scale = None
707
- if shared_scale:
708
- max_width = max((frame.size[0] for frame in cropped_frames), default=0)
709
- max_height = max((frame.size[1] for frame in cropped_frames), default=0)
710
- if max_width > 0 and max_height > 0:
711
- common_scale = min(cell_size / max_width, cell_size / max_height) * fit_scale
712
-
713
- frames: list[Image.Image] = []
714
- for index, frame in enumerate(cropped_frames):
715
- frame_width, frame_height = frame.size
716
- canvas = Image.new("RGBA", (cell_size, cell_size), (0, 0, 0, 0))
717
- if frame_width > 0 and frame_height > 0:
718
- scale = common_scale or (
719
- min(cell_size / frame_width, cell_size / frame_height) * fit_scale
720
- )
721
- new_width = max(1, int(frame_width * scale))
722
- new_height = max(1, int(frame_height * scale))
723
- frame = frame.resize((new_width, new_height), Image.Resampling.LANCZOS)
724
- paste_x = (cell_size - new_width) // 2
725
- if align in {"bottom", "feet"}:
726
- pad = max(0, int(cell_size * (1 - fit_scale) * 0.5))
727
- paste_y = cell_size - new_height - pad
728
- else:
729
- paste_y = (cell_size - new_height) // 2
730
- canvas.paste(frame, (paste_x, paste_y))
731
- frame_info[index]["output_size"] = [new_width, new_height]
732
- frame_info[index]["paste_position"] = [paste_x, paste_y]
733
- else:
734
- frame_info[index]["output_size"] = [0, 0]
735
- frame_info[index]["paste_position"] = [0, 0]
736
- frames.append(canvas)
737
- return frames, frame_info
738
-
739
-
740
- def compose_sheet(frames: list[Image.Image], rows: int, cols: int, cell_size: int) -> Image.Image:
741
- canvas = Image.new("RGBA", (cols * cell_size, rows * cell_size), (0, 0, 0, 0))
742
- for index, frame in enumerate(frames):
743
- row, col = divmod(index, cols)
744
- canvas.paste(frame, (col * cell_size, row * cell_size), frame)
745
- return canvas
746
-
747
-
748
- def save_transparent_gif(frames: list[Image.Image], out_path: Path, duration: int) -> None:
749
- if not frames:
750
- raise ValueError("No frames to encode.")
751
-
752
- key = (255, 0, 254)
753
- width, height = frames[0].size
754
- stacked = Image.new("RGB", (width, height * len(frames)), key)
755
-
756
- for index, frame in enumerate(frames):
757
- r, g, b, a = frame.split()
758
- hard_mask = a.point(lambda value: 255 if value >= 128 else 0)
759
- rgb = Image.merge("RGB", (r, g, b))
760
- stacked.paste(rgb, (0, index * height), hard_mask)
761
-
762
- paletted = stacked.convert(
763
- "P", palette=Image.Palette.ADAPTIVE, colors=256, dither=Image.Dither.NONE
764
- )
765
- palette = list(paletted.getpalette() or [])
766
- while len(palette) < 256 * 3:
767
- palette.append(0)
768
-
769
- key_index = None
770
- for index in range(256):
771
- if palette[index * 3 : index * 3 + 3] == list(key):
772
- key_index = index
773
- break
774
- if key_index is None:
775
- best_distance = None
776
- best_index = 0
777
- for index in range(256):
778
- r, g, b = palette[index * 3], palette[index * 3 + 1], palette[index * 3 + 2]
779
- distance = (r - key[0]) ** 2 + (g - key[1]) ** 2 + (b - key[2]) ** 2
780
- if best_distance is None or distance < best_distance:
781
- best_distance = distance
782
- best_index = index
783
- key_index = best_index
784
-
785
- if key_index != 0:
786
- lut = np.arange(256, dtype=np.uint8)
787
- lut[0], lut[key_index] = key_index, 0
788
- arr = np.array(paletted)
789
- arr = lut[arr]
790
- paletted = Image.fromarray(arr, mode="P")
791
- for channel in range(3):
792
- zero_idx = channel
793
- key_idx = key_index * 3 + channel
794
- palette[zero_idx], palette[key_idx] = palette[key_idx], palette[zero_idx]
795
- paletted.putpalette(palette)
796
-
797
- out_frames = [
798
- paletted.crop((0, index * height, width, (index + 1) * height))
799
- for index in range(len(frames))
800
- ]
801
- out_frames[0].save(
802
- out_path,
803
- format="GIF",
804
- save_all=True,
805
- append_images=out_frames[1:],
806
- duration=duration,
807
- loop=0,
808
- disposal=2,
809
- transparency=0,
810
- background=0,
811
- )
812
-
813
-
814
- def sanitize_slug(text: str) -> str:
815
- slug = re.sub(r"[^a-zA-Z0-9]+", "-", text.strip().lower()).strip("-")
816
- return slug or "sprite"
817
-
818
-
819
- def cmd_list_options() -> None:
820
- print(
821
- json.dumps(
822
- {
823
- "targets": TARGET_MODES,
824
- "npc_roles": NPC_ROLES,
825
- "grid_shapes": GRID_SHAPES,
826
- "frame_labels": FRAME_LABELS,
827
- "processor": {
828
- "component_mode": ["all", "largest"],
829
- "align": ["center", "bottom", "feet"],
830
- },
831
- },
832
- indent=2,
833
- )
834
- )
835
-
836
-
837
- def cmd_build_prompt(args: argparse.Namespace) -> None:
838
- prompt_text, seed = build_prompt(args.target, args.mode, args.prompt, args.role, args.seed)
839
- payload = {
840
- "target": args.target,
841
- "mode": args.mode,
842
- "prompt": args.prompt,
843
- "role": args.role or "",
844
- "seed": seed,
845
- "generated_prompt": prompt_text,
846
- }
847
- if args.write:
848
- args.write.parent.mkdir(parents=True, exist_ok=True)
849
- args.write.write_text(prompt_text, encoding="utf-8")
850
- if args.write_json:
851
- args.write_json.parent.mkdir(parents=True, exist_ok=True)
852
- args.write_json.write_text(json.dumps(payload, indent=2), encoding="utf-8")
853
- print(prompt_text)
854
-
855
-
856
- def cmd_process(args: argparse.Namespace) -> None:
857
- if args.target not in PROCESS_TARGETS:
858
- raise ValueError(
859
- f"Unknown process target '{args.target}'. Valid targets: {', '.join(PROCESS_TARGETS)}"
860
- )
861
- out_dir = args.output_dir
862
- out_dir.mkdir(parents=True, exist_ok=True)
863
-
864
- raw = Image.open(args.input).convert("RGBA")
865
- metadata = {
866
- "target": args.target,
867
- "mode": args.mode,
868
- "prompt": args.prompt or "",
869
- "role": args.role or "",
870
- "input": str(args.input),
871
- "threshold": args.threshold,
872
- "edge_threshold": args.edge_threshold,
873
- "duration": args.duration,
874
- }
875
-
876
- has_custom_grid = args.rows is not None or args.cols is not None
877
- if has_custom_grid and (args.rows is None or args.cols is None):
878
- raise ValueError("Custom grids require both --rows and --cols.")
879
-
880
- use_grid = has_custom_grid or args.mode in GRID_SHAPES
881
- if not use_grid and args.mode not in SINGLE_MODES:
882
- known = ", ".join(sorted(GRID_SHAPES))
883
- singles = ", ".join(sorted(SINGLE_MODES))
884
- raise ValueError(
885
- f"Mode '{args.mode}' is not a known grid shape and --rows/--cols "
886
- f"were not provided. Pass both --rows and --cols for a custom grid, "
887
- f"use a known grid mode ({known}), or a single-frame mode ({singles})."
888
- )
889
-
890
- if use_grid:
891
- if has_custom_grid:
892
- rows, cols = args.rows, args.cols
893
- else:
894
- rows, cols = GRID_SHAPES[args.mode]
895
- cell_size = args.cell_size or (96 if (rows, cols) == (4, 4) else 128)
896
- raw.save(out_dir / "raw-sheet.png")
897
- cleaned = remove_bg_magenta(raw.copy(), args.threshold, args.edge_threshold)
898
- cleaned.save(out_dir / "raw-sheet-clean.png")
899
-
900
- frames, frame_qc = split_grid(
901
- raw,
902
- rows,
903
- cols,
904
- cell_size,
905
- args.threshold,
906
- args.edge_threshold,
907
- fit_scale=args.fit_scale,
908
- trim_border_px=args.trim_border,
909
- edge_clean_depth=args.edge_clean_depth,
910
- align=args.align,
911
- shared_scale=args.shared_scale,
912
- component_mode=args.component_mode,
913
- component_padding=args.component_padding,
914
- min_component_area=args.min_component_area,
915
- edge_touch_margin=args.edge_touch_margin,
916
- )
917
- if has_custom_grid:
918
- prefix = args.label_prefix or args.mode
919
- labels = [f"{prefix}-{index + 1}" for index in range(rows * cols)]
920
- else:
921
- labels = FRAME_LABELS[args.mode]
922
- for label, frame in zip(labels, frames):
923
- frame.save(out_dir / f"{label}.png")
924
-
925
- compose_sheet(frames, rows, cols, cell_size).save(out_dir / "sheet-transparent.png")
926
-
927
- if args.mode == "player_sheet" and not has_custom_grid and (rows, cols) == (4, 4):
928
- directions = ["down", "left", "right", "up"]
929
- for row_index, direction in enumerate(directions):
930
- row_frames = frames[row_index * cols : (row_index + 1) * cols]
931
- compose_sheet(row_frames, 1, cols, cell_size).save(
932
- out_dir / f"{direction}-strip.png"
933
- )
934
- save_transparent_gif(row_frames, out_dir / f"{direction}.gif", args.duration)
935
- metadata["directions"] = directions
936
- else:
937
- save_transparent_gif(frames, out_dir / "animation.gif", args.duration)
938
-
939
- metadata["rows"] = rows
940
- metadata["cols"] = cols
941
- metadata["cell_size"] = cell_size
942
- metadata["fit_scale"] = args.fit_scale
943
- metadata["trim_border"] = args.trim_border
944
- metadata["edge_clean_depth"] = args.edge_clean_depth
945
- metadata["align"] = args.align
946
- metadata["shared_scale"] = args.shared_scale
947
- metadata["component_mode"] = args.component_mode
948
- metadata["component_padding"] = args.component_padding
949
- metadata["min_component_area"] = args.min_component_area
950
- metadata["edge_touch_margin"] = args.edge_touch_margin
951
- metadata["frame_labels"] = labels
952
- metadata["frames"] = frame_qc
953
- metadata["edge_touch_frames"] = [
954
- info["grid"] for info in frame_qc if bool(info.get("edge_touch"))
955
- ]
956
- if args.reject_edge_touch and metadata["edge_touch_frames"]:
957
- raise ValueError(f"Frames touch a cell edge: {metadata['edge_touch_frames']}")
958
- else:
959
- raw.save(out_dir / "raw.png")
960
- centered = center_single_sprite(raw, args.single_size, args.threshold, args.edge_threshold)
961
- centered.save(out_dir / "clean.png")
962
- metadata["single_size"] = args.single_size
963
-
964
- if args.prompt_file and args.prompt_file.exists():
965
- prompt_text = args.prompt_file.read_text(encoding="utf-8")
966
- (out_dir / "prompt-used.txt").write_text(prompt_text, encoding="utf-8")
967
- elif args.prompt:
968
- (out_dir / "prompt-used.txt").write_text(args.prompt, encoding="utf-8")
969
-
970
- (out_dir / "pipeline-meta.json").write_text(json.dumps(metadata, indent=2), encoding="utf-8")
971
- print(str(out_dir.resolve()))
972
-
973
-
974
- def build_parser() -> argparse.ArgumentParser:
975
- parser = argparse.ArgumentParser(description=__doc__)
976
- subparsers = parser.add_subparsers(dest="command", required=True)
977
-
978
- subparsers.add_parser("list-options", help="Print supported targets, modes, and NPC roles.")
979
-
980
- build_prompt_parser = subparsers.add_parser("build-prompt", help="Build a generation prompt.")
981
- build_prompt_parser.add_argument("--target", required=True, choices=sorted(TARGET_MODES))
982
- build_prompt_parser.add_argument("--mode", required=True)
983
- build_prompt_parser.add_argument("--prompt", required=True)
984
- build_prompt_parser.add_argument("--role")
985
- build_prompt_parser.add_argument("--seed", type=int)
986
- build_prompt_parser.add_argument("--write", type=Path)
987
- build_prompt_parser.add_argument("--write-json", type=Path)
988
-
989
- process_parser = subparsers.add_parser("process", help="Postprocess a generated sprite image.")
990
- process_parser.add_argument("--input", required=True, type=Path)
991
- process_parser.add_argument("--target", required=True, choices=PROCESS_TARGETS)
992
- process_parser.add_argument("--mode", required=True)
993
- process_parser.add_argument("--output-dir", required=True, type=Path)
994
- process_parser.add_argument("--role")
995
- process_parser.add_argument("--prompt")
996
- process_parser.add_argument("--prompt-file", type=Path)
997
- process_parser.add_argument("--threshold", type=int, default=100)
998
- process_parser.add_argument("--edge-threshold", type=int, default=150)
999
- process_parser.add_argument("--cell-size", type=int)
1000
- process_parser.add_argument("--rows", type=int)
1001
- process_parser.add_argument("--cols", type=int)
1002
- process_parser.add_argument("--label-prefix")
1003
- process_parser.add_argument("--fit-scale", type=float, default=0.85)
1004
- process_parser.add_argument("--trim-border", type=int, default=4)
1005
- process_parser.add_argument("--edge-clean-depth", type=int, default=3)
1006
- process_parser.add_argument("--align", choices=["center", "bottom", "feet"], default="center")
1007
- process_parser.add_argument("--shared-scale", action="store_true")
1008
- process_parser.add_argument("--component-mode", choices=["all", "largest"], default="all")
1009
- process_parser.add_argument("--component-padding", type=int, default=0)
1010
- process_parser.add_argument("--min-component-area", type=int, default=1)
1011
- process_parser.add_argument("--edge-touch-margin", type=int, default=0)
1012
- process_parser.add_argument("--reject-edge-touch", action="store_true")
1013
- process_parser.add_argument("--single-size", type=int, default=256)
1014
- process_parser.add_argument("--duration", type=int, default=200)
1015
-
1016
- return parser
1017
-
1018
-
1019
- def main() -> None:
1020
- parser = build_parser()
1021
- args = parser.parse_args()
1022
- if args.command == "list-options":
1023
- cmd_list_options()
1024
- elif args.command == "build-prompt":
1025
- cmd_build_prompt(args)
1026
- else:
1027
- cmd_process(args)
1028
-
1029
-
1030
- if __name__ == "__main__":
1031
- main()