castle-web-cli 0.4.90 → 0.4.92
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 +23 -4
- package/dist/metering.d.ts +21 -0
- package/dist/metering.js +75 -0
- package/dist/native/loop.js +1 -0
- package/dist/native/openrouter.d.ts +1 -0
- package/dist/native/openrouter.js +1 -0
- package/dist/native/types.d.ts +1 -0
- package/kits/basic-2d/editors/SceneEditor.jsx +39 -6
- package/kits/basic-2d/engine/scene.js +10 -2
- package/kits/physics-2d/behaviors/Collider.jsx +190 -153
- package/kits/physics-2d/editors/SceneEditor.jsx +42 -6
- package/kits/physics-2d/editors/SelectionOverlay.jsx +57 -28
- package/kits/physics-2d/engine/collider.js +123 -105
- package/kits/physics-2d/engine/scene.js +10 -2
- package/kits/physics-2d/physics/behaviors/RigidBody.jsx +3 -0
- package/kits/physics-2d/physics/matterBridge.js +73 -24
- package/package.json +1 -1
- package/kits/physics-2d/engine/behaviorExtensions.js +0 -28
- package/kits/physics-2d/physics/extensions/collider.js +0 -15
package/dist/agent.js
CHANGED
|
@@ -24,6 +24,7 @@ import { rawDataToString } from "./ide.js";
|
|
|
24
24
|
import { buildRouterPrompt, buildTaskPrompt, userTurnInstruction, CLAUDE_TASK_SYSTEM_REMINDER, } from "./agent-prompts.js";
|
|
25
25
|
import { checkOpenrouterKey, checkOpenrouterModel, openrouterCatalogEntry, primeOpenrouterCatalog, } from "./openrouter-catalog.js";
|
|
26
26
|
import { classifyProviderError, failureCopy, } from "./agent-failures.js";
|
|
27
|
+
import { meteringHeaders, newAgentSessionId, withCustomHeaders, } from "./metering.js";
|
|
27
28
|
import { runAgentNative } from "./native/loop.js";
|
|
28
29
|
import { createPlaytestBrowserManager } from "./native/playtest-browser.js";
|
|
29
30
|
import { createPlaywrightPlaytestExecutor } from "./native/playtest-executor.js";
|
|
@@ -343,7 +344,11 @@ const OPENROUTER_ALLOWED_TOOLS = "--allowedTools=Edit,Write,NotebookEdit,Bash";
|
|
|
343
344
|
function buildAgentInvocation(backend, role, prompt, claudeModel,
|
|
344
345
|
// Already resolved for this role by the caller (router turns pass
|
|
345
346
|
// settings.routerOpenrouterModel, task spawns settings.tasksOpenrouterModel).
|
|
346
|
-
openrouterModel
|
|
347
|
+
openrouterModel,
|
|
348
|
+
// This run's ledger identity, and the deck dir its castle.json is read from.
|
|
349
|
+
// Only the claude branch can carry it: cursor-agent runs on its own key and
|
|
350
|
+
// never traverses the llm-proxy.
|
|
351
|
+
metering) {
|
|
347
352
|
if (backend === "claude") {
|
|
348
353
|
const viaOpenrouter = claudeModel === "openrouter";
|
|
349
354
|
return {
|
|
@@ -379,9 +384,13 @@ openrouterModel) {
|
|
|
379
384
|
: []),
|
|
380
385
|
prompt,
|
|
381
386
|
],
|
|
382
|
-
env: viaOpenrouter
|
|
387
|
+
env: withCustomHeaders(viaOpenrouter
|
|
383
388
|
? envForOpenrouterSpawn(openrouterApiKey())
|
|
384
|
-
: envForAgentSpawn(backend),
|
|
389
|
+
: envForAgentSpawn(backend), meteringHeaders({
|
|
390
|
+
deckDir: metering.deckDir,
|
|
391
|
+
sessionId: metering.sessionId,
|
|
392
|
+
route: viaOpenrouter ? "openrouter" : "anthropic",
|
|
393
|
+
})),
|
|
385
394
|
};
|
|
386
395
|
}
|
|
387
396
|
return {
|
|
@@ -1456,6 +1465,7 @@ async function runAgentSmith(opts) {
|
|
|
1456
1465
|
const result = await runAgentNative({
|
|
1457
1466
|
cwd: opts.cwd,
|
|
1458
1467
|
role: opts.role,
|
|
1468
|
+
extraHeaders: opts.extraHeaders,
|
|
1459
1469
|
model: opts.model,
|
|
1460
1470
|
apiKey: openrouterApiKey(),
|
|
1461
1471
|
reasoningEffort: opts.openrouterTuning?.reasoningEffort,
|
|
@@ -1574,10 +1584,19 @@ async function runAgentTurn(opts) {
|
|
|
1574
1584
|
crashed: false,
|
|
1575
1585
|
};
|
|
1576
1586
|
}
|
|
1587
|
+
// One id per call: a router turn, or a single task attempt (the retry loop
|
|
1588
|
+
// in runTaskAgentIn calls this once per attempt, and each attempt is its own
|
|
1589
|
+
// conversation -- nothing is resumed).
|
|
1590
|
+
const sessionId = newAgentSessionId(opts.role);
|
|
1577
1591
|
if (opts.backend === "smith") {
|
|
1578
1592
|
return runAgentSmith({
|
|
1579
1593
|
cwd: opts.cwd,
|
|
1580
1594
|
role: opts.role,
|
|
1595
|
+
extraHeaders: meteringHeaders({
|
|
1596
|
+
deckDir: opts.cwd,
|
|
1597
|
+
sessionId,
|
|
1598
|
+
route: "openrouter",
|
|
1599
|
+
}),
|
|
1581
1600
|
model: opts.openrouterModel,
|
|
1582
1601
|
prompt: opts.prompt,
|
|
1583
1602
|
// Mirrors claude's --append-system-prompt for tasks (the native loop
|
|
@@ -1596,7 +1615,7 @@ async function runAgentTurn(opts) {
|
|
|
1596
1615
|
onSpawn: opts.onSpawn,
|
|
1597
1616
|
});
|
|
1598
1617
|
}
|
|
1599
|
-
const invocation = buildAgentInvocation(opts.backend, opts.role, opts.prompt, opts.claudeModel, opts.openrouterModel);
|
|
1618
|
+
const invocation = buildAgentInvocation(opts.backend, opts.role, opts.prompt, opts.claudeModel, opts.openrouterModel, { sessionId, deckDir: opts.cwd });
|
|
1600
1619
|
return runAgentCli({
|
|
1601
1620
|
cwd: opts.cwd,
|
|
1602
1621
|
command: invocation.command,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type MeteringRoute = "anthropic" | "openrouter";
|
|
2
|
+
/**
|
|
3
|
+
* A fresh id for one agent run -- one router turn (one user message) or one
|
|
4
|
+
* task attempt. Every provider call in that run's agentic loop carries it, so
|
|
5
|
+
* the ledger can price a single turn even though concurrent tasks interleave
|
|
6
|
+
* with chat in wall-clock time. The role prefix keeps "chat spend vs.
|
|
7
|
+
* background-task spend" a prefix match with no extra column.
|
|
8
|
+
*/
|
|
9
|
+
export declare function newAgentSessionId(role: "router" | "task"): string;
|
|
10
|
+
export declare function meteringHeaders(opts: {
|
|
11
|
+
deckDir: string;
|
|
12
|
+
sessionId: string;
|
|
13
|
+
route: MeteringRoute;
|
|
14
|
+
}): Record<string, string>;
|
|
15
|
+
/**
|
|
16
|
+
* Merge metering headers into a claude spawn env. ANTHROPIC_CUSTOM_HEADERS is
|
|
17
|
+
* the claude CLI's newline-separated `Name: Value` list, forwarded on every
|
|
18
|
+
* request it makes to ANTHROPIC_BASE_URL. An inherited value is kept, first,
|
|
19
|
+
* rather than clobbered.
|
|
20
|
+
*/
|
|
21
|
+
export declare function withCustomHeaders(env: NodeJS.ProcessEnv, headers: Record<string, string>): NodeJS.ProcessEnv;
|
package/dist/metering.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Per-request metering identity for agent runs inside a Castle sandbox.
|
|
2
|
+
//
|
|
3
|
+
// The per-host llm-proxy (castle-sandboxes/llm-proxy) terminates every
|
|
4
|
+
// provider call a sandbox makes, records a usage row, and strips these two
|
|
5
|
+
// headers before forwarding upstream -- so they never reach Anthropic or
|
|
6
|
+
// OpenRouter. Names must match LLM_SESSION_HEADER / LLM_DECK_HEADER in
|
|
7
|
+
// castle-sandboxes/shared/src/index.ts.
|
|
8
|
+
//
|
|
9
|
+
// The proxy already knows the user (its token is minted per sandbox) and the
|
|
10
|
+
// sandbox. These headers add the two dimensions only this process knows: which
|
|
11
|
+
// deck the work belongs to, and which agent run issued the call.
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { randomBytes } from "crypto";
|
|
15
|
+
const SESSION_HEADER = "x-castle-agent-session-id";
|
|
16
|
+
const DECK_HEADER = "x-castle-deck-id";
|
|
17
|
+
/**
|
|
18
|
+
* A fresh id for one agent run -- one router turn (one user message) or one
|
|
19
|
+
* task attempt. Every provider call in that run's agentic loop carries it, so
|
|
20
|
+
* the ledger can price a single turn even though concurrent tasks interleave
|
|
21
|
+
* with chat in wall-clock time. The role prefix keeps "chat spend vs.
|
|
22
|
+
* background-task spend" a prefix match with no extra column.
|
|
23
|
+
*/
|
|
24
|
+
export function newAgentSessionId(role) {
|
|
25
|
+
return `${role === "router" ? "r" : "t"}-${randomBytes(12).toString("hex")}`;
|
|
26
|
+
}
|
|
27
|
+
// Outside a sandbox nothing injects a base URL, and the headers would be sent
|
|
28
|
+
// to the provider itself -- handing a deck id to a third party to no purpose.
|
|
29
|
+
function proxyInjected(route) {
|
|
30
|
+
return Boolean(route === "anthropic"
|
|
31
|
+
? process.env.ANTHROPIC_BASE_URL
|
|
32
|
+
: process.env.OPENROUTER_BASE_URL);
|
|
33
|
+
}
|
|
34
|
+
// castle.json is written by the first save (see save-deck.ts), so an unsaved
|
|
35
|
+
// deck has no Castle id and its spend stays unattributed rather than being
|
|
36
|
+
// labelled with the sandbox-local directory name, which never becomes the
|
|
37
|
+
// deck id. Re-read per run: a deck saved mid-session starts attributing on its
|
|
38
|
+
// next turn.
|
|
39
|
+
function deckIdFor(deckDir) {
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(fs.readFileSync(path.join(deckDir, "castle.json"), "utf8"));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (typeof parsed.deckId !== "string" || !parsed.deckId)
|
|
48
|
+
return null;
|
|
49
|
+
return parsed.deckId.replace(/[\r\n]/g, "") || null;
|
|
50
|
+
}
|
|
51
|
+
export function meteringHeaders(opts) {
|
|
52
|
+
if (!proxyInjected(opts.route))
|
|
53
|
+
return {};
|
|
54
|
+
const deckId = deckIdFor(opts.deckDir);
|
|
55
|
+
return {
|
|
56
|
+
[SESSION_HEADER]: opts.sessionId,
|
|
57
|
+
...(deckId ? { [DECK_HEADER]: deckId } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Merge metering headers into a claude spawn env. ANTHROPIC_CUSTOM_HEADERS is
|
|
62
|
+
* the claude CLI's newline-separated `Name: Value` list, forwarded on every
|
|
63
|
+
* request it makes to ANTHROPIC_BASE_URL. An inherited value is kept, first,
|
|
64
|
+
* rather than clobbered.
|
|
65
|
+
*/
|
|
66
|
+
export function withCustomHeaders(env, headers) {
|
|
67
|
+
const lines = Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
68
|
+
if (lines.length === 0)
|
|
69
|
+
return env;
|
|
70
|
+
const inherited = env.ANTHROPIC_CUSTOM_HEADERS;
|
|
71
|
+
return {
|
|
72
|
+
...env,
|
|
73
|
+
ANTHROPIC_CUSTOM_HEADERS: [...(inherited ? [inherited] : []), ...lines].join("\n"),
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/native/loop.js
CHANGED
|
@@ -607,6 +607,7 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
607
607
|
reasoningEffort: opts.reasoningEffort ?? REASONING_EFFORT[opts.role],
|
|
608
608
|
routing: opts.routing,
|
|
609
609
|
providerTier: opts.providerTier,
|
|
610
|
+
extraHeaders: opts.extraHeaders,
|
|
610
611
|
maxTokens: MAX_COMPLETION_TOKENS,
|
|
611
612
|
signal: controller.signal,
|
|
612
613
|
onDelta: opts.onDelta,
|
package/dist/native/types.d.ts
CHANGED
|
@@ -797,7 +797,9 @@ function useSelectionGesture(args) {
|
|
|
797
797
|
const point = { x: raw.x + cam.x, y: raw.y + cam.y };
|
|
798
798
|
current.canvasRef.current.setPointerCapture(event.pointerId);
|
|
799
799
|
const scene = makeScene(current.sceneData, behaviorClasses, current.sprites, current.files);
|
|
800
|
-
const
|
|
800
|
+
const stack = scene.actorsAt(point.x, point.y);
|
|
801
|
+
const stackIds = stack.map((a) => a.id);
|
|
802
|
+
const actor = stack[0] ?? null;
|
|
801
803
|
const drag = {
|
|
802
804
|
pointerId: event.pointerId,
|
|
803
805
|
startPoint: point,
|
|
@@ -820,7 +822,7 @@ function useSelectionGesture(args) {
|
|
|
820
822
|
} else if (current.multiSelectMode) {
|
|
821
823
|
handleModePointerDown(drag, actor, current);
|
|
822
824
|
} else {
|
|
823
|
-
handleDefaultPointerDown(drag, actor, point, current);
|
|
825
|
+
handleDefaultPointerDown(drag, actor, point, current, stackIds);
|
|
824
826
|
}
|
|
825
827
|
drag.moveStarts = collectMoveStarts(current.sceneData, drag.movingActorIds);
|
|
826
828
|
dragRef.current = drag;
|
|
@@ -891,6 +893,13 @@ function useSelectionGesture(args) {
|
|
|
891
893
|
finalizeMarquee(drag, current);
|
|
892
894
|
} else if (drag.kind === 'idle' && !drag.movedFar && !drag.longPressFired) {
|
|
893
895
|
handleTap(drag, current);
|
|
896
|
+
} else if (
|
|
897
|
+
drag.kind === 'move' &&
|
|
898
|
+
!drag.movedFar &&
|
|
899
|
+
!drag.longPressFired &&
|
|
900
|
+
drag.cycleStack
|
|
901
|
+
) {
|
|
902
|
+
cycleSelection(drag, current);
|
|
894
903
|
}
|
|
895
904
|
current.marqueeRef.current = null;
|
|
896
905
|
dragRef.current = null;
|
|
@@ -1198,13 +1207,23 @@ function handleModePointerDown(drag, actor, current) {
|
|
|
1198
1207
|
drag.pendingMarquee = true;
|
|
1199
1208
|
}
|
|
1200
1209
|
}
|
|
1201
|
-
function handleDefaultPointerDown(drag, actor, point, current) {
|
|
1210
|
+
function handleDefaultPointerDown(drag, actor, point, current, stackIds) {
|
|
1202
1211
|
if (actor) {
|
|
1203
|
-
|
|
1212
|
+
const sel = current.selectedActorIds;
|
|
1213
|
+
if (sel.length === 1 && stackIds.length > 1 && stackIds.includes(sel[0])) {
|
|
1214
|
+
// Overlapping pile with a single selected actor under the cursor: keep it
|
|
1215
|
+
// selected so it stays draggable, and arm click-to-cycle so a stationary
|
|
1216
|
+
// click descends to the next actor beneath it (see cycleSelection).
|
|
1217
|
+
drag.movingActorIds = [...sel];
|
|
1218
|
+
drag.cycleStack = stackIds;
|
|
1219
|
+
} else if (sel.includes(actor.id)) {
|
|
1220
|
+
// Pressed an actor that's part of the current selection: keep the
|
|
1221
|
+
// selection so the whole group stays draggable.
|
|
1222
|
+
drag.movingActorIds = [...sel];
|
|
1223
|
+
} else {
|
|
1224
|
+
// Fresh pick: select the topmost actor under the cursor.
|
|
1204
1225
|
current.onSelectActorIds([actor.id]);
|
|
1205
1226
|
drag.movingActorIds = [actor.id];
|
|
1206
|
-
} else {
|
|
1207
|
-
drag.movingActorIds = [...current.selectedActorIds];
|
|
1208
1227
|
}
|
|
1209
1228
|
drag.kind = 'move';
|
|
1210
1229
|
drag.longPressTimer = window.setTimeout(() => {
|
|
@@ -1235,6 +1254,20 @@ function finalizeMarquee(drag, current) {
|
|
|
1235
1254
|
for (const id of hits) merged.add(id);
|
|
1236
1255
|
current.onSelectActorIds([...merged]);
|
|
1237
1256
|
}
|
|
1257
|
+
// A stationary click on a pile of overlapping actors advances the selection to
|
|
1258
|
+
// the next actor below the currently selected one, wrapping around at the
|
|
1259
|
+
// bottom. This lets repeated clicks in the same spot reach an actor buried under
|
|
1260
|
+
// others that would otherwise always win the topmost hit-test.
|
|
1261
|
+
function cycleSelection(drag, current) {
|
|
1262
|
+
const stack = drag.cycleStack;
|
|
1263
|
+
if (!stack || stack.length < 2) return;
|
|
1264
|
+
const sel = current.selectedActorIds;
|
|
1265
|
+
if (sel.length !== 1) return;
|
|
1266
|
+
const idx = stack.indexOf(sel[0]);
|
|
1267
|
+
if (idx === -1) return;
|
|
1268
|
+
const nextId = stack[(idx + 1) % stack.length];
|
|
1269
|
+
if (nextId !== sel[0]) current.onSelectActorIds([nextId]);
|
|
1270
|
+
}
|
|
1238
1271
|
function handleTap(drag, current) {
|
|
1239
1272
|
if (!drag.modeAtStart) return;
|
|
1240
1273
|
if (drag.startedOnActorId !== null) {
|
|
@@ -281,7 +281,15 @@ export class SceneRuntime {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
actorAt(x, y) {
|
|
284
|
+
return this.actorsAt(x, y)[0] ?? null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// All actors whose Layout box contains the point, ordered topmost-first (high
|
|
288
|
+
// z -> low z). Used by the editor's click-to-cycle so repeated clicks in the
|
|
289
|
+
// same spot can walk down through overlapping actors.
|
|
290
|
+
actorsAt(x, y) {
|
|
284
291
|
const actors = this.getActors().slice().reverse();
|
|
292
|
+
const hits = [];
|
|
285
293
|
for (const actor of actors) {
|
|
286
294
|
const layout = getLayout(actor);
|
|
287
295
|
if (!layout) continue;
|
|
@@ -291,10 +299,10 @@ export class SceneRuntime {
|
|
|
291
299
|
y >= layout.y &&
|
|
292
300
|
y <= layout.y + layout.height
|
|
293
301
|
) {
|
|
294
|
-
|
|
302
|
+
hits.push(actor);
|
|
295
303
|
}
|
|
296
304
|
}
|
|
297
|
-
return
|
|
305
|
+
return hits;
|
|
298
306
|
}
|
|
299
307
|
|
|
300
308
|
actorIdsInRect(rect) {
|