castle-web-cli 0.4.55 → 0.4.57

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/dist/agent.js CHANGED
@@ -78,6 +78,10 @@ function buildAgentInvocation(backend, role, prompt, claudeModel) {
78
78
  const ROUTER_TIMEOUT_MS = 3 * 60_000;
79
79
  const TASK_TIMEOUT_MS = 30 * 60_000;
80
80
  const MAX_TASK_ATTEMPTS = 3;
81
+ // Cap on task agents running at once -- keeps us under e2b / provider rate
82
+ // limits. Over-cap tasks stay queued ('waiting') and start, earliest-created
83
+ // first, as running ones finish. Conservative default; override via env.
84
+ const MAX_CONCURRENT_TASKS = Number(process.env.CASTLE_MAX_CONCURRENT_TASKS) || 4;
81
85
  const TASK_POLL_MS = 1_000;
82
86
  const FENCE_HOLDBACK = '```castle-';
83
87
  const RESULT_SUMMARY_CHARS = 600;
@@ -497,9 +501,21 @@ function createTaskStore(opts) {
497
501
  return !dep || isTerminal(dep.status);
498
502
  });
499
503
  }
504
+ function runningCount() {
505
+ let n = 0;
506
+ for (const t of tasks.values())
507
+ if (t.status === 'running')
508
+ n++;
509
+ return n;
510
+ }
500
511
  function maybeStart(task) {
501
512
  if (task.status !== 'waiting' || task.acknowledged || !depsAreSettled(task))
502
513
  return;
514
+ // Concurrency cap: at most MAX_CONCURRENT_TASKS agents run at once. Over-cap
515
+ // tasks stay 'waiting' and are restarted -- earliest-created first -- by the
516
+ // onFinished sweep below when a running task frees a slot.
517
+ if (runningCount() >= MAX_CONCURRENT_TASKS)
518
+ return;
503
519
  start(task);
504
520
  }
505
521
  function start(task) {
@@ -540,7 +556,9 @@ function createTaskStore(opts) {
540
556
  : `${result.error ?? 'failed'}\n${result.finalText.slice(-RESULT_SUMMARY_CHARS)}`;
541
557
  touch(task);
542
558
  opts.onFinished(task);
543
- for (const waiting of tasks.values())
559
+ // A slot just freed -- restart eligible waiting tasks, earliest-created
560
+ // first, up to the concurrency cap.
561
+ for (const waiting of sorted())
544
562
  maybeStart(waiting);
545
563
  });
546
564
  }
package/dist/bundle.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import * as path from 'path';
2
2
  import { build } from 'vite';
3
3
  import { viteSingleFile } from 'vite-plugin-singlefile';
4
+ import { sceneFilesPlugin } from './vitePlugins.js';
4
5
  export async function bundleProject(dir) {
5
6
  const result = await build({
6
7
  root: dir,
7
- plugins: [viteSingleFile()],
8
+ plugins: [viteSingleFile(), sceneFilesPlugin()],
8
9
  build: {
9
10
  write: false,
10
11
  rollupOptions: {
package/dist/serve.js CHANGED
@@ -6,6 +6,7 @@ import { createServer } from 'vite';
6
6
  import { WebSocketServer, WebSocket } from 'ws';
7
7
  import { createIdeServer, DECK_FOCUS_SCRIPT } from './ide.js';
8
8
  import { createAgentServer } from './agent.js';
9
+ import { sceneFilesPlugin } from './vitePlugins.js';
9
10
  import * as config from './config.js';
10
11
  function isPortFree(port) {
11
12
  return new Promise((resolve) => {
@@ -194,7 +195,7 @@ export async function serve(dir, options = {}) {
194
195
  process.on('exit', () => agentServer.shutdown());
195
196
  const vite = await createServer({
196
197
  root: projectDir,
197
- plugins: [castlePlugin(wsPort, ideServer, agentServer)],
198
+ plugins: [castlePlugin(wsPort, ideServer, agentServer), sceneFilesPlugin()],
198
199
  server: {
199
200
  port,
200
201
  strictPort: true,
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare function sceneFilesPlugin(): Plugin;
@@ -0,0 +1,27 @@
1
+ import * as fs from 'fs';
2
+ // Safety net for `.scene` / `.drawing` files imported as JS modules.
3
+ //
4
+ // These files are DATA (JSON), not modules. The blessed way to read them is the
5
+ // engine's file APIs (`scene.readFromFile(name)` / the `?raw` glob in
6
+ // engine/files.js). But agents sometimes still write a plain
7
+ // `import scene from './scenes/main.scene'`, which Vite would otherwise serve as
8
+ // raw JSON-as-JS and the browser throws "Unexpected token ':'". This plugin
9
+ // makes that import resolve to the parsed object so it just works.
10
+ //
11
+ // `?raw` (and any other query suffix) is left untouched so the engine's
12
+ // `import.meta.glob(..., { query: '?raw' })` still gets raw text.
13
+ export function sceneFilesPlugin() {
14
+ return {
15
+ name: 'castle-scene-files',
16
+ load(id) {
17
+ const [filePath, query] = id.split('?');
18
+ if (query !== undefined)
19
+ return null;
20
+ if (filePath.endsWith('.scene') || filePath.endsWith('.drawing')) {
21
+ const text = fs.readFileSync(filePath, 'utf-8');
22
+ return `export default JSON.parse(${JSON.stringify(text)});\n`;
23
+ }
24
+ return null;
25
+ },
26
+ };
27
+ }
@@ -17,7 +17,7 @@ The deck is already serving when you start (`castle-web init` set that up; see `
17
17
  1. **Build incrementally.** Start with the smallest playable thing (one mechanic, one scene change), `npm run restart`, then add the next piece. Do NOT write the whole game in one shot.
18
18
  2. **After every edit:** `npm run restart` (no hot reload). The served page refreshes and the user sees the change.
19
19
  3. **Prefer real, editable assets.** For game objects, characters, and scenery, make actual drawings (`drawings/*.drawing`) and place them as real actors in `scenes/*.scene` — not shapes drawn in code. Real assets let the creator move and re-skin things in the editor and let other creators remix the deck. Data-driven UI (health bars, score/text, HUD gauges) and dynamic things (bullets, particles, effects) are correctly procedural/code — don't force those into drawings.
20
- 4. **Separate scenes per screen.** Use a separate `scenes/*.scene` file for each distinct screen — menu/title, each level, game-over, etc. — not one mega-scene. Each stays independently editable in the editor. Switch at runtime with `scene.load(targetSceneData)`; wire the transition deck-side (e.g. import the target `.scene` JSON and call `scene.load` on play / win / level change).
20
+ 4. **Separate scenes per screen.** Use a separate `scenes/*.scene` file for each distinct screen — menu/title, each level, game-over, etc. — not one mega-scene. Each stays independently editable in the editor. Switch at runtime with `scene.loadFromFile('gameover.scene')` (reads the file and transitions) on play / win / level change. NEVER `import` a `.scene` file as a module — scene files are data, not modules; use `scene.readFromFile` / `scene.loadFromFile`.
21
21
 
22
22
  Card size is **500 wide × 700 tall** (origin top-left, +y is down).
23
23
 
@@ -107,6 +107,9 @@ Rules: every actor needs a unique `id` (any string) and almost always a `Layout`
107
107
  - `scene.spawnActor({ components: { Layout: {...}, MyBehavior: {...} } })` — add a new actor at runtime. Returns the actor (with auto-minted `id` and `runtime = {}`). Use this; don't push to `scene.data.actors` by hand.
108
108
  - `scene.despawnActor(id)` — remove an actor at runtime. Use this; don't `splice` + `delete` by hand.
109
109
  - `scene.status` — string you can set/read for game-state ('playing', 'gameover', ...).
110
+ - `scene.load(sceneData)` — replace the running scene with the given scene data object.
111
+ - `scene.readFromFile(name)` — read and parse a scene file (`'gameover.scene'`, `'levels/2.scene'`), returning its scene data. Use this instead of importing a `.scene` file.
112
+ - `scene.loadFromFile(name)` — read a scene file and transition the running scene to it. The way to switch screens/levels and to restart, e.g. `scene.loadFromFile('main.scene')`.
110
113
  - `actor.runtime` — per-instance scratchpad for transient state across frames (e.g. velocity, trail history). Not serialized.
111
114
 
112
115
  ## Input shortcuts
@@ -1,8 +1,23 @@
1
+ import { initialFiles, parseJsonFile } from './files';
2
+
1
3
  const CARD_WIDTH = 500;
2
4
  const CARD_HEIGHT = 700;
3
5
 
4
6
  export const cardSize = { width: CARD_WIDTH, height: CARD_HEIGHT };
5
7
 
8
+ // Resolve a scene-file name to its key in the loaded file map. Accepts
9
+ // 'main.scene', 'scenes/main.scene', './scenes/main.scene', or 'main' -- the
10
+ // `.scene` extension and a leading `scenes/` are both optional.
11
+ function resolveSceneFileKey(name) {
12
+ const key = String(name).replace(/^\.?\//, '');
13
+ const candidates = [key];
14
+ if (!key.endsWith('.scene')) candidates.push(`${key}.scene`);
15
+ for (const candidate of [...candidates]) {
16
+ if (!candidate.startsWith('scenes/')) candidates.push(`scenes/${candidate}`);
17
+ }
18
+ return candidates.find((candidate) => initialFiles[candidate] !== undefined) ?? key;
19
+ }
20
+
6
21
  export class SceneRuntime {
7
22
  constructor(sceneData, behaviors, drawings) {
8
23
  this.behaviors = new Map(behaviors.map((Behavior) => [Behavior.behaviorName, Behavior]));
@@ -35,6 +50,26 @@ export class SceneRuntime {
35
50
  }
36
51
  }
37
52
 
53
+ // Read and parse a scene file by name, returning its scene data. Use this
54
+ // instead of `import x from './scenes/foo.scene'` -- scene files are data, not
55
+ // JS modules, so importing them is unsupported.
56
+ readFromFile(name) {
57
+ const key = resolveSceneFileKey(name);
58
+ const text = initialFiles[key];
59
+ if (text === undefined) {
60
+ throw new Error(`scene file not found: ${name}`);
61
+ }
62
+ const { value, error } = parseJsonFile(key, text);
63
+ if (error) throw new Error(error);
64
+ return value;
65
+ }
66
+
67
+ // Transition the running scene to the scene stored in the named file (read it
68
+ // fresh, then load it). The building block for "go to scene" / restart.
69
+ loadFromFile(name) {
70
+ this.load(this.readFromFile(name));
71
+ }
72
+
38
73
  clone() {
39
74
  return new SceneRuntime(this.serialize(), [...this.behaviors.values()], this.drawings);
40
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.55",
3
+ "version": "0.4.57",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"