castle-web-cli 0.4.172 → 0.4.174
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-prompts.d.ts +8 -2
- package/dist/agent-prompts.js +19 -11
- package/dist/agent.js +37 -13
- package/dist/assetCompression.d.ts +3 -0
- package/dist/assetCompression.js +30 -0
- package/dist/castleJson.d.ts +1 -1
- package/dist/castleJson.js +2 -2
- package/dist/imports.js +2 -2
- package/dist/init.js +4 -4
- package/dist/native/loop.js +9 -0
- package/dist/native/openrouter.d.ts +2 -0
- package/dist/native/openrouter.js +10 -1
- package/dist/native/types.d.ts +1 -0
- package/dist/serve.js +2 -0
- package/dist/serveSecurity.d.ts +2 -0
- package/dist/serveSecurity.js +17 -3
- package/dist/shell/assets/{index-Ws0WrCbi.js → index-DF1yMXPS.js} +4 -4
- package/dist/shell/index.html +1 -1
- package/kits/base/castle.json +1 -1
- package/kits/multiplayer-2d/castle.json +2 -2
- package/kits/multiplayer-2d/code/server/sceneBodies.js +1 -1
- package/kits/multiplayer-2d/code/server/world.js +2 -2
- package/kits/multiplayer-2d/code/systems/multiplayer.js +12 -0
- package/kits/multiplayer-2d/fonts.generated.js +1 -1
- package/kits/multiplayer-2d/package-lock.json +1 -1
- package/kits/multiplayer-3d/CLAUDE.md +5 -4
- package/kits/multiplayer-3d/castle.json +3 -3
- package/kits/multiplayer-3d/code/client/avatars.js +47 -18
- package/kits/multiplayer-3d/code/client/nameTags.js +16 -35
- package/kits/multiplayer-3d/code/server/sceneBodies.js +1 -1
- package/kits/multiplayer-3d/code/systems/multiplayer.js +14 -1
- package/kits/multiplayer-3d/fonts.generated.js +1 -1
- package/kits/multiplayer-3d/package-lock.json +1 -1
- package/kits/physics-2d/CLAUDE.md +7 -6
- package/kits/physics-2d/behaviors/Collider.jsx +7 -1
- package/kits/physics-2d/behaviors/RigidBody.jsx +6 -3
- package/kits/physics-2d/castle.json +2 -2
- package/kits/physics-2d/editors/SceneEditor.jsx +1 -0
- package/kits/physics-2d/editors/SingleEditor.jsx +4 -4
- package/kits/physics-2d/editors/{StyleEditor.jsx → ThemeEditor.jsx} +8 -8
- package/kits/physics-2d/editors/deckFont.js +2 -2
- package/kits/physics-2d/editors/{styleEditor.module.css → themeEditor.module.css} +2 -2
- package/kits/physics-2d/engine/avatarArt.js +108 -14
- package/kits/physics-2d/engine/files.js +2 -2
- package/kits/physics-2d/engine/physics/matterBridge.js +2 -2
- package/kits/physics-2d/engine/scene.js +1 -1
- package/kits/physics-2d/fonts.generated.js +1 -1
- package/kits/physics-2d/package-lock.json +1 -1
- package/kits/physics-3d/behaviors/Body.jsx +10 -1
- package/kits/physics-3d/behaviors/Door.jsx +5 -0
- package/kits/physics-3d/behaviors/Lookable.jsx +3 -0
- package/kits/physics-3d/behaviors/Model.jsx +4 -0
- package/kits/physics-3d/behaviors/Pickup.jsx +4 -0
- package/kits/physics-3d/behaviors/Player.jsx +7 -0
- package/kits/physics-3d/behaviors/Shape.jsx +13 -0
- package/kits/physics-3d/castle.json +1 -1
- package/kits/physics-3d/engine3d/editor3dInspector.jsx +1 -0
- package/kits/physics-3d/fonts.generated.js +1 -1
- package/kits/physics-3d/package-lock.json +1 -1
- package/kits/real-time/castle.json +1 -1
- package/kits/real-time/code/client/connection.js +11 -2
- package/kits/real-time/package-lock.json +1 -1
- package/kits/turn-based/castle.json +1 -1
- package/kits/turn-based/fonts.generated.js +1 -1
- package/kits/turn-based/package-lock.json +1 -1
- package/package.json +3 -1
- /package/kits/physics-2d/editors/{styleTheme.js → theme.js} +0 -0
package/dist/agent-prompts.d.ts
CHANGED
|
@@ -75,7 +75,11 @@ export declare function renderPlanDigest(plan: RouterPlanOpts, tasks: PromptTask
|
|
|
75
75
|
export declare function planOpenQuestionLines(fileText: string): string[];
|
|
76
76
|
export declare function renderTaskPlanSlice(fileText: string, item: string | undefined, finishedWork?: PlanFinishedWork[]): string;
|
|
77
77
|
export declare const NO_DECK_DOCS = "This deck has no CLAUDE.md / AGENTS.md quick reference. There is no documentation to read -- do not search for one (CLAUDE.md, README, **/*.md, etc.).";
|
|
78
|
-
export
|
|
78
|
+
export interface RouterPromptParts {
|
|
79
|
+
system: string;
|
|
80
|
+
user: string;
|
|
81
|
+
}
|
|
82
|
+
export interface RouterPromptOpts {
|
|
79
83
|
deckLabel: string;
|
|
80
84
|
quickReference?: string;
|
|
81
85
|
deckTree?: string;
|
|
@@ -85,7 +89,9 @@ export declare function buildRouterPrompt(opts: {
|
|
|
85
89
|
plan?: RouterPlanOpts;
|
|
86
90
|
playtest?: boolean;
|
|
87
91
|
instruction: string;
|
|
88
|
-
}
|
|
92
|
+
}
|
|
93
|
+
export declare function buildRouterPromptParts(opts: RouterPromptOpts): RouterPromptParts;
|
|
94
|
+
export declare function buildRouterPrompt(opts: RouterPromptOpts): string;
|
|
89
95
|
export declare function userTurnInstruction(opts: {
|
|
90
96
|
messages: string[];
|
|
91
97
|
interruptedDraft?: string;
|
package/dist/agent-prompts.js
CHANGED
|
@@ -820,7 +820,7 @@ function renderTasks(tasks, showItems) {
|
|
|
820
820
|
// not included" and burns its first turns hunting for CLAUDE.md. Exported so
|
|
821
821
|
// the platform block (platformDoc.ts) can keep the sentence after itself.
|
|
822
822
|
export const NO_DECK_DOCS = "This deck has no CLAUDE.md / AGENTS.md quick reference. There is no documentation to read -- do not search for one (CLAUDE.md, README, **/*.md, etc.).";
|
|
823
|
-
export function
|
|
823
|
+
export function buildRouterPromptParts(opts) {
|
|
824
824
|
// Making the ABSENCE explicit, not just omitting the section, matters: a
|
|
825
825
|
// router on a bare/greenfield deck otherwise has no way to tell "no docs
|
|
826
826
|
// exist" from "the docs section just wasn't included in this prompt", and
|
|
@@ -842,17 +842,19 @@ export function buildRouterPrompt(opts) {
|
|
|
842
842
|
const plan = opts.plan
|
|
843
843
|
? `\n\n== plan ==\n${renderPlanDigest(opts.plan, opts.tasks)}`
|
|
844
844
|
: "";
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
|
|
845
|
+
// Two parts, split for prompt caching. `system` holds what is the same from
|
|
846
|
+
// one turn to the next within a serve -- the rules, the deck identity, the
|
|
847
|
+
// file tree and (smith) the deck source -- and each backend sends it as
|
|
848
|
+
// system text, which is the prefix the provider caches turn after turn. A
|
|
849
|
+
// file edit invalidates it; a chat turn does not. `user` holds what changes
|
|
850
|
+
// every turn: the transcript, the plan, the board and this instruction.
|
|
851
|
+
return {
|
|
852
|
+
system: `${routerRules(opts.plan !== undefined, opts.playtest === true)}
|
|
850
853
|
|
|
851
854
|
== deck ==
|
|
852
|
-
${opts.deckLabel}${quickReference}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
${renderTranscript(opts.messages)}${deckFiles}${deckSource}${plan}
|
|
855
|
+
${opts.deckLabel}${quickReference}${deckFiles}${deckSource}`,
|
|
856
|
+
user: `== conversation so far ==
|
|
857
|
+
${renderTranscript(opts.messages)}${plan}
|
|
856
858
|
|
|
857
859
|
== background tasks ==
|
|
858
860
|
${renderTasks(opts.tasks, opts.plan !== undefined)}
|
|
@@ -860,7 +862,13 @@ ${renderTasks(opts.tasks, opts.plan !== undefined)}
|
|
|
860
862
|
== now ==
|
|
861
863
|
${opts.instruction}
|
|
862
864
|
|
|
863
|
-
Reply now, as "you" in the conversation. Plain reply text only -- no role prefix
|
|
865
|
+
Reply now, as "you" in the conversation. Plain reply text only -- no role prefix.`,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
// The router prompt as one string, system part first. What the goldens pin.
|
|
869
|
+
export function buildRouterPrompt(opts) {
|
|
870
|
+
const parts = buildRouterPromptParts(opts);
|
|
871
|
+
return `${parts.system}\n\n${parts.user}`;
|
|
864
872
|
}
|
|
865
873
|
export function userTurnInstruction(opts) {
|
|
866
874
|
const parts = [];
|
package/dist/agent.js
CHANGED
|
@@ -27,7 +27,7 @@ import { WebSocketServer } from 'ws';
|
|
|
27
27
|
import { rawDataToString } from './rawData.js';
|
|
28
28
|
import { atomicWriteFileSync } from './atomicFile.js';
|
|
29
29
|
import { AGENT_ATTACHMENT_PREFIX, AGENT_PLAYTEST_PREFIX, PLAN_FILE } from './localPaths.js';
|
|
30
|
-
import { applyPlanOps,
|
|
30
|
+
import { applyPlanOps, buildRouterPromptParts, buildTaskPrompt, parsePlanOps, planOpenQuestionLines, truncateToBytes, userTurnInstruction, CLAUDE_TASK_SYSTEM_REMINDER, } from './agent-prompts.js';
|
|
31
31
|
import { readCastleJson } from './castleJson.js';
|
|
32
32
|
import { checkOpenrouterKey, checkOpenrouterModel, openrouterCatalogEntry, primeOpenrouterCatalog, } from './openrouter-catalog.js';
|
|
33
33
|
import { classifyProviderError, failureCopy, setReaderTimeZone, } from './agent-failures.js';
|
|
@@ -501,6 +501,17 @@ function claudeSettingsArg(auth) {
|
|
|
501
501
|
...(auth?.mode === 'user-key' ? { apiKeyHelper: anthropicKeyHelperCommand() } : {}),
|
|
502
502
|
});
|
|
503
503
|
}
|
|
504
|
+
// The claude CLI puts every CLAUDE.md it finds, and a git-status note, into the
|
|
505
|
+
// first user message of each process, ahead of the prompt. A router turn is a
|
|
506
|
+
// new process, so that text (the kit guide: ~23k tokens on a kit deck) is sent
|
|
507
|
+
// uncached on every call of every turn. The router already carries the deck's
|
|
508
|
+
// quick reference and file tree in its cached system part, and can read a
|
|
509
|
+
// guide on demand; tasks keep the CLI's own loading. Measured on a 30k-token
|
|
510
|
+
// router prompt: a second turn goes from 40k tokens written to 4.6k.
|
|
511
|
+
const ROUTER_CLAUDE_ENV = {
|
|
512
|
+
CLAUDE_CODE_DISABLE_CLAUDE_MDS: '1',
|
|
513
|
+
CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS: '1',
|
|
514
|
+
};
|
|
504
515
|
function buildAgentInvocation(backend, role, prompt, claudeModel,
|
|
505
516
|
// Already resolved for this role by the caller (router turns pass
|
|
506
517
|
// settings.routerOpenrouterModel, task spawns settings.tasksOpenrouterModel).
|
|
@@ -512,7 +523,14 @@ metering,
|
|
|
512
523
|
// The task this spawn belongs to, so its playtest frames land under
|
|
513
524
|
// tasks/<id>/playtest/ and count against that task's call budget. Absent for
|
|
514
525
|
// router turns, which get no playtest tool at all.
|
|
515
|
-
taskId
|
|
526
|
+
taskId,
|
|
527
|
+
// The stable part of a router prompt. claude takes it as appended system
|
|
528
|
+
// text, which its own cache breakpoints cover; cursor has no such flag and
|
|
529
|
+
// gets it ahead of the prompt.
|
|
530
|
+
systemPrompt) {
|
|
531
|
+
const systemText = [systemPrompt, role === 'task' ? CLAUDE_TASK_SYSTEM_REMINDER : undefined]
|
|
532
|
+
.filter((text) => Boolean(text))
|
|
533
|
+
.join('\n\n');
|
|
516
534
|
if (backend === 'claude') {
|
|
517
535
|
const viaOpenrouter = claudeModel === 'openrouter';
|
|
518
536
|
const orAuth = viaOpenrouter ? resolveOpenrouterAuth() : null;
|
|
@@ -547,15 +565,18 @@ taskId) {
|
|
|
547
565
|
claudeSettingsArg(anAuth),
|
|
548
566
|
'--strict-mcp-config',
|
|
549
567
|
...(role === 'task' ? [mcpConfigArg(metering.deckDir, taskId)] : []),
|
|
550
|
-
...(
|
|
568
|
+
...(systemText ? ['--append-system-prompt', systemText] : []),
|
|
551
569
|
prompt,
|
|
552
570
|
],
|
|
553
|
-
env:
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
571
|
+
env: {
|
|
572
|
+
...withCustomHeaders(viaOpenrouter ? envForOpenrouterSpawn(orAuth) : envForClaudeSpawn(anAuth), meteringHeaders({
|
|
573
|
+
deckDir: metering.deckDir,
|
|
574
|
+
sessionId: metering.sessionId,
|
|
575
|
+
route: viaOpenrouter ? 'openrouter' : 'anthropic',
|
|
576
|
+
direct,
|
|
577
|
+
})),
|
|
578
|
+
...(role === 'router' ? ROUTER_CLAUDE_ENV : {}),
|
|
579
|
+
},
|
|
559
580
|
};
|
|
560
581
|
}
|
|
561
582
|
return {
|
|
@@ -569,7 +590,7 @@ taskId) {
|
|
|
569
590
|
'--model',
|
|
570
591
|
CURSOR_MODEL,
|
|
571
592
|
...(role === 'router' ? ['--mode', 'ask'] : ['--force']),
|
|
572
|
-
prompt,
|
|
593
|
+
systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt,
|
|
573
594
|
],
|
|
574
595
|
env: envForAgentSpawn(backend),
|
|
575
596
|
};
|
|
@@ -1852,6 +1873,7 @@ async function runAgentSmith(opts) {
|
|
|
1852
1873
|
// "" (auto) becomes undefined so no provider.order is sent.
|
|
1853
1874
|
providerTier: opts.openrouterTuning?.providerTier || undefined,
|
|
1854
1875
|
prompt: opts.prompt,
|
|
1876
|
+
system: opts.system,
|
|
1855
1877
|
systemReminder: opts.systemReminder,
|
|
1856
1878
|
attachments: opts.attachments,
|
|
1857
1879
|
timeoutMs: opts.timeoutMs,
|
|
@@ -2169,6 +2191,7 @@ async function runAgentTurn(opts) {
|
|
|
2169
2191
|
}),
|
|
2170
2192
|
model: opts.openrouterModel,
|
|
2171
2193
|
prompt: opts.prompt,
|
|
2194
|
+
system: opts.systemPrompt,
|
|
2172
2195
|
// Mirrors claude's --append-system-prompt for tasks (the native loop
|
|
2173
2196
|
// appends it to its own system framing).
|
|
2174
2197
|
systemReminder: opts.role === 'task' ? CLAUDE_TASK_SYSTEM_REMINDER : undefined,
|
|
@@ -2185,7 +2208,7 @@ async function runAgentTurn(opts) {
|
|
|
2185
2208
|
onSpawn: opts.onSpawn,
|
|
2186
2209
|
}));
|
|
2187
2210
|
}
|
|
2188
|
-
const invocation = buildAgentInvocation(opts.backend, opts.role, opts.prompt, opts.claudeModel, opts.openrouterModel, { sessionId, deckDir: opts.cwd }, opts.taskId);
|
|
2211
|
+
const invocation = buildAgentInvocation(opts.backend, opts.role, opts.prompt, opts.claudeModel, opts.openrouterModel, { sessionId, deckDir: opts.cwd }, opts.taskId, opts.systemPrompt);
|
|
2189
2212
|
const startedMs = Date.now();
|
|
2190
2213
|
const run = runAgentCli({
|
|
2191
2214
|
cwd: opts.cwd,
|
|
@@ -3273,7 +3296,7 @@ function routerTurnPrompt(ctx, instruction, selfMessageId, plan) {
|
|
|
3273
3296
|
// ROUTER_DECK_CONTENTS_BUDGET's comment (the router prompt is already the
|
|
3274
3297
|
// largest one this serve builds).
|
|
3275
3298
|
const isSmith = ctx.backend() === 'smith';
|
|
3276
|
-
return
|
|
3299
|
+
return buildRouterPromptParts({
|
|
3277
3300
|
deckLabel: ctx.deckLabel,
|
|
3278
3301
|
quickReference: quickReferenceFor(ctx.deckDir, ctx.quickReference, ctx.backend()),
|
|
3279
3302
|
deckTree: buildDeckTree(ctx.deckDir, isSmith
|
|
@@ -3409,7 +3432,8 @@ function runRouterTurnIn(ctx, instruction, attachments = []) {
|
|
|
3409
3432
|
void runAgentTurn({
|
|
3410
3433
|
backend,
|
|
3411
3434
|
role: 'router',
|
|
3412
|
-
prompt,
|
|
3435
|
+
prompt: prompt.user,
|
|
3436
|
+
systemPrompt: prompt.system,
|
|
3413
3437
|
claudeModel: ctx.claudeModel(),
|
|
3414
3438
|
openrouterModel: ctx.openrouterModel(),
|
|
3415
3439
|
openrouterTuning: ctx.openrouterTuning(),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import compression from 'compression';
|
|
2
|
+
/** Compress assets, never the token-bearing shell bootstrap or control API. */
|
|
3
|
+
export function createAssetCompression() {
|
|
4
|
+
// compression supports plain Node HTTP/Connect; its DefinitelyTyped signature
|
|
5
|
+
// unnecessarily requires Express request/response extensions.
|
|
6
|
+
return compression({
|
|
7
|
+
threshold: 1024,
|
|
8
|
+
filter(req, res) {
|
|
9
|
+
const requestPath = (req.url || '/').split('?')[0];
|
|
10
|
+
if ((req.method !== 'GET' && req.method !== 'HEAD') ||
|
|
11
|
+
requestPath === '/' ||
|
|
12
|
+
(requestPath.startsWith('/__castle/') &&
|
|
13
|
+
!requestPath.startsWith('/__castle/ide/assets/')) ||
|
|
14
|
+
req.headers.range ||
|
|
15
|
+
res.hasHeader('Content-Range')) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
// Conditional responses must retain the representation's Vary header,
|
|
19
|
+
// but must never start a compressor or acquire a body.
|
|
20
|
+
if (res.statusCode === 304) {
|
|
21
|
+
if (res.getHeader('Vary') !== '*')
|
|
22
|
+
res.appendHeader('Vary', 'Accept-Encoding');
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return (res.statusCode === 200 &&
|
|
26
|
+
!String(res.getHeader('Content-Type')).startsWith('text/event-stream') &&
|
|
27
|
+
compression.filter(req, res));
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
package/dist/castleJson.d.ts
CHANGED
package/dist/castleJson.js
CHANGED
|
@@ -5,7 +5,7 @@ import picomatch from 'picomatch';
|
|
|
5
5
|
// module with no imports of its own, because both the import machinery and the
|
|
6
6
|
// fork overlay need it and importing it from either of those would close a
|
|
7
7
|
// cycle.
|
|
8
|
-
// Root-level
|
|
8
|
+
// Root-level theme files a kit ships as a deck's starting point, seeded into a
|
|
9
9
|
// deck at scaffold (init.ts `writeStarterTheme`) and when an import newly brings
|
|
10
10
|
// them (imports.ts `seedKitTheme`).
|
|
11
11
|
//
|
|
@@ -18,7 +18,7 @@ import picomatch from 'picomatch';
|
|
|
18
18
|
// `theme.style` names the deck's font, `fonts.generated.js` is what carries that
|
|
19
19
|
// face's bytes into the bundle, and a deck given the first without the second
|
|
20
20
|
// renders in the fallback while claiming otherwise.
|
|
21
|
-
export const
|
|
21
|
+
export const STARTER_THEME_FILES = ['theme.style', 'fonts.generated.js'];
|
|
22
22
|
export const IMPORTS_DIR = 'imports';
|
|
23
23
|
export const DEFAULT_STARTER_SCENE = 'scenes/main.scene';
|
|
24
24
|
export const DEFAULT_SERVER_TICK_RATE = 20;
|
package/dist/imports.js
CHANGED
|
@@ -6,7 +6,7 @@ import { runTar } from './save-deck.js';
|
|
|
6
6
|
import { normalizeDeckPackageJson } from './normalize.js';
|
|
7
7
|
import { getKitsDir } from './localPaths.js';
|
|
8
8
|
import { writePlatformDocs } from './platformDoc.js';
|
|
9
|
-
import { ensureVisibleGlob, IMPORTS_DIR,
|
|
9
|
+
import { ensureVisibleGlob, IMPORTS_DIR, STARTER_THEME_FILES, readCastleJson as tryReadCastleJson, readCastleJsonOrThrow as readCastleJson, } from './castleJson.js';
|
|
10
10
|
// Adding another deck as a dependency (`castle-web add-import`; removing one is a
|
|
11
11
|
// later command). Deliberately NOT `get-deck`: that one
|
|
12
12
|
// replaces THIS deck's own source from the server (and carries guards for the
|
|
@@ -614,7 +614,7 @@ function seedKitTheme(deckDir, alias, log) {
|
|
|
614
614
|
if (typeof main !== 'string' || !main.trim())
|
|
615
615
|
return;
|
|
616
616
|
const seeded = [];
|
|
617
|
-
for (const name of
|
|
617
|
+
for (const name of STARTER_THEME_FILES) {
|
|
618
618
|
const src = path.join(importDir, name);
|
|
619
619
|
const dest = path.join(deckDir, name);
|
|
620
620
|
if (!fs.existsSync(src) || fs.existsSync(dest))
|
package/dist/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { CLAUDE_MD_PLATFORM_SECTION } from './platformDoc.js';
|
|
4
4
|
import { installDeps } from './install.js';
|
|
5
|
-
import { deckMainFile, deckStarterScene,
|
|
5
|
+
import { deckMainFile, deckStarterScene, STARTER_THEME_FILES } from './castleJson.js';
|
|
6
6
|
import { addImportTo, IMPORTS_DIR, lockImportTree, restoreMissingImports, syncImportDependencies, } from './imports.js';
|
|
7
7
|
import { discardParentSource, fetchParentSource, unpackParentSource, writeRemixIdentity, } from './remix.js';
|
|
8
8
|
import { getCliEntryPath, getKitsDir, getSdkPackagePath, toPosixPath } from './localPaths.js';
|
|
@@ -309,7 +309,7 @@ export function writeStarterScene(projectDir, kitDir, alias) {
|
|
|
309
309
|
fs.mkdirSync(path.join(projectDir, 'scenes'), { recursive: true });
|
|
310
310
|
writeJsonFile(path.join(projectDir, 'scenes', 'main.scene'), rewrite(scene));
|
|
311
311
|
}
|
|
312
|
-
// A kit may ship starter
|
|
312
|
+
// A kit may ship starter theme files (deck-level theme: the color palette,
|
|
313
313
|
// the deck font). Like the starter scene, they are deck content: the engine
|
|
314
314
|
// reads them from the DECK root, and they are the creator's to edit -- so they
|
|
315
315
|
// get seeded once at scaffold time rather than living read-only in the import.
|
|
@@ -317,11 +317,11 @@ export function writeStarterScene(projectDir, kitDir, alias) {
|
|
|
317
317
|
// kit file paths, so no ref rewriting). Exported for the same two scaffold paths
|
|
318
318
|
// as writeStarterScene.
|
|
319
319
|
//
|
|
320
|
-
// The set is
|
|
320
|
+
// The set is STARTER_THEME_FILES (castleJson.ts), which says why these two
|
|
321
321
|
// travel together: a deck given the record without the generated module renders
|
|
322
322
|
// in the fallback while claiming otherwise.
|
|
323
323
|
export function writeStarterTheme(projectDir, kitDir) {
|
|
324
|
-
for (const name of
|
|
324
|
+
for (const name of STARTER_THEME_FILES) {
|
|
325
325
|
const src = path.join(kitDir, name);
|
|
326
326
|
const dest = path.join(projectDir, name);
|
|
327
327
|
if (!fs.existsSync(src) || fs.existsSync(dest))
|
package/dist/native/loop.js
CHANGED
|
@@ -319,6 +319,8 @@ const ROLE_FRAMING = {
|
|
|
319
319
|
};
|
|
320
320
|
function buildSystemMessage(opts) {
|
|
321
321
|
const parts = [ROLE_FRAMING[opts.role]];
|
|
322
|
+
if (opts.system)
|
|
323
|
+
parts.push(opts.system);
|
|
322
324
|
if (opts.systemReminder)
|
|
323
325
|
parts.push(opts.systemReminder);
|
|
324
326
|
return parts.join("\n\n");
|
|
@@ -700,10 +702,17 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
700
702
|
};
|
|
701
703
|
}
|
|
702
704
|
usedAnyTool = true;
|
|
705
|
+
// The reasoning goes back with the turn it belongs to. Anthropic treats an
|
|
706
|
+
// assistant turn returned without its thinking as a changed prefix and
|
|
707
|
+
// recomputes every message after it, so without this no call in the loop
|
|
708
|
+
// reads the cache (measured: 0 cached tokens per call, 30k written each).
|
|
703
709
|
messages.push({
|
|
704
710
|
role: "assistant",
|
|
705
711
|
content: streamResult.message.content || null,
|
|
706
712
|
tool_calls: toolCalls,
|
|
713
|
+
...(streamResult.message.reasoning_details
|
|
714
|
+
? { reasoning_details: streamResult.message.reasoning_details }
|
|
715
|
+
: {}),
|
|
707
716
|
});
|
|
708
717
|
const toolResults = await runToolCalls(toolCalls, opts.role, toolCtx, playtestFrames, toolLabels, imageLabels, log, opts.onActivity);
|
|
709
718
|
messages.push(...toolResults);
|
|
@@ -25,11 +25,13 @@ export interface ORMessage {
|
|
|
25
25
|
content?: string | null | ORContentPart[];
|
|
26
26
|
tool_calls?: ORToolCall[];
|
|
27
27
|
tool_call_id?: string;
|
|
28
|
+
reasoning_details?: unknown[];
|
|
28
29
|
}
|
|
29
30
|
export interface ORAssistantMessage {
|
|
30
31
|
role: "assistant";
|
|
31
32
|
content: string;
|
|
32
33
|
tool_calls?: ORToolCall[];
|
|
34
|
+
reasoning_details?: unknown[];
|
|
33
35
|
}
|
|
34
36
|
export type ORReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
35
37
|
export type ORRoutingMode = "balanced" | "nitro" | "exacto" | "floor";
|
|
@@ -344,6 +344,7 @@ export async function streamChatCompletion(opts) {
|
|
|
344
344
|
return { message: null, crashed: false, error: "could not run openrouter: empty response body" };
|
|
345
345
|
}
|
|
346
346
|
let content = "";
|
|
347
|
+
const reasoningDetails = [];
|
|
347
348
|
// Observed live (2026-07): some providers' MID-stream usage passthrough is
|
|
348
349
|
// junk (constant cache_read=128 / cache_created=0 on every event) while the
|
|
349
350
|
// FINAL usage chunk -- the one OpenRouter emits at/after finish_reason --
|
|
@@ -398,6 +399,9 @@ export async function streamChatCompletion(opts) {
|
|
|
398
399
|
if (typeof delta.reasoning === "string" && delta.reasoning) {
|
|
399
400
|
opts.onThinking?.(delta.reasoning);
|
|
400
401
|
}
|
|
402
|
+
if (Array.isArray(delta.reasoning_details)) {
|
|
403
|
+
reasoningDetails.push(...delta.reasoning_details);
|
|
404
|
+
}
|
|
401
405
|
const toolCallDeltas = delta.tool_calls;
|
|
402
406
|
if (Array.isArray(toolCallDeltas)) {
|
|
403
407
|
for (const tc of toolCallDeltas) {
|
|
@@ -461,7 +465,12 @@ export async function streamChatCompletion(opts) {
|
|
|
461
465
|
};
|
|
462
466
|
}
|
|
463
467
|
return {
|
|
464
|
-
message: {
|
|
468
|
+
message: {
|
|
469
|
+
role: "assistant",
|
|
470
|
+
content,
|
|
471
|
+
tool_calls: finalizeToolCalls(pendingToolCalls),
|
|
472
|
+
...(reasoningDetails.length ? { reasoning_details: reasoningDetails } : {}),
|
|
473
|
+
},
|
|
465
474
|
usage: finalUsage ?? usage,
|
|
466
475
|
reasoningTokens: finalReasoningTokens ?? reasoningTokens,
|
|
467
476
|
crashed: false,
|
package/dist/native/types.d.ts
CHANGED
package/dist/serve.js
CHANGED
|
@@ -19,6 +19,7 @@ import { graphql as castleGraphql } from './api.js';
|
|
|
19
19
|
import { executeCommand } from './castle-host/host.js';
|
|
20
20
|
import { ServeSecurity } from './serveSecurity.js';
|
|
21
21
|
import { isSameOriginUpgrade } from './wsOrigin.js';
|
|
22
|
+
import { createAssetCompression } from './assetCompression.js';
|
|
22
23
|
import { resolveServerConfig } from './castleJson.js';
|
|
23
24
|
import { startDevSessionServer, } from './devSessionServer.js';
|
|
24
25
|
// The log is a tail, not an archive: append, and once it passes the cap drop
|
|
@@ -315,6 +316,7 @@ window.__castleShellOrigin=(function(){
|
|
|
315
316
|
},
|
|
316
317
|
},
|
|
317
318
|
configureServer(server) {
|
|
319
|
+
server.middlewares.use(createAssetCompression());
|
|
318
320
|
server.middlewares.use((req, res, next) => {
|
|
319
321
|
const reqPath = (req.url || '/').split('?')[0];
|
|
320
322
|
const trustedSurface = req.headers['x-castle-sandbox-surface'];
|
package/dist/serveSecurity.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare class ServeSecurity {
|
|
|
18
18
|
private readonly contentProxyToken;
|
|
19
19
|
private readonly signingKey;
|
|
20
20
|
private readonly serveNonce;
|
|
21
|
+
private contentGrantExpiresAtSeconds;
|
|
21
22
|
private readonly pathKey;
|
|
22
23
|
private readonly tickets;
|
|
23
24
|
private localContentPorts;
|
|
@@ -50,6 +51,7 @@ export declare class ServeSecurity {
|
|
|
50
51
|
editOrigin: string;
|
|
51
52
|
playOrigin: string;
|
|
52
53
|
};
|
|
54
|
+
private contentGrantExpiry;
|
|
53
55
|
/**
|
|
54
56
|
* Origin a browser should use for a local content role.
|
|
55
57
|
*
|
package/dist/serveSecurity.js
CHANGED
|
@@ -6,6 +6,7 @@ const CROCKFORD = '0123456789abcdefghjkmnpqrstvwxyz';
|
|
|
6
6
|
const CONTENT_LABEL_RE = /^1([ep])([0-9a-z]{1,4})-([a-z0-9]{14})-([0-9a-z]{1,8})-([0-9a-hjkmnp-tv-z]{13})-([0-9a-hjkmnp-tv-z]{20})$/;
|
|
7
7
|
const MAX_GRANT_SECONDS = 12 * 60 * 60;
|
|
8
8
|
const GRANT_CLOCK_SKEW_SECONDS = 60;
|
|
9
|
+
const MIN_CONTENT_GRANT_REMAINING_SECONDS = 11 * 60 * 60;
|
|
9
10
|
const BROWSER_GRANT_SECONDS = 3 * 60;
|
|
10
11
|
export const MOBILE_HOST_PROTOCOL = '2';
|
|
11
12
|
// A signature covers the request path and nothing else, so every path it can
|
|
@@ -79,6 +80,7 @@ export class ServeSecurity {
|
|
|
79
80
|
contentProxyToken = randomBytes(32).toString('base64url');
|
|
80
81
|
signingKey = process.env.CASTLE_AGENT_TOKEN || randomBytes(32).toString('base64url');
|
|
81
82
|
serveNonce = base32Lower(randomBytes(8));
|
|
83
|
+
contentGrantExpiresAtSeconds = 0;
|
|
82
84
|
pathKey = randomBytes(32).toString('base64url');
|
|
83
85
|
tickets = new Map();
|
|
84
86
|
localContentPorts;
|
|
@@ -284,9 +286,7 @@ window.fetch=function(input,init){
|
|
|
284
286
|
: cloud
|
|
285
287
|
? 'sandboxes.castlexyz.com'
|
|
286
288
|
: null;
|
|
287
|
-
|
|
288
|
-
// enforces the hard 12-hour maximum when it resolves the capability.
|
|
289
|
-
const expiresAtSeconds = Math.floor(Date.now() / 1000) + MAX_GRANT_SECONDS - GRANT_CLOCK_SKEW_SECONDS;
|
|
289
|
+
const expiresAtSeconds = cloud && contentDomain ? this.contentGrantExpiry() : 0;
|
|
290
290
|
const originFor = (role) => {
|
|
291
291
|
// Safari on macOS 15 asks DNS to resolve `*.localhost`, where it fails.
|
|
292
292
|
// Dedicated loopback ports are still distinct browser origins and need no DNS.
|
|
@@ -297,6 +297,20 @@ window.fetch=function(input,init){
|
|
|
297
297
|
};
|
|
298
298
|
return { editOrigin: originFor('edit'), playOrigin: originFor('play') };
|
|
299
299
|
}
|
|
300
|
+
contentGrantExpiry() {
|
|
301
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
302
|
+
const latestExpiry = nowSeconds + MAX_GRANT_SECONDS - GRANT_CLOCK_SKEW_SECONDS;
|
|
303
|
+
// Expiry is part of the hostname: reuse it so reopening the shell can reuse
|
|
304
|
+
// the browser's module cache. Until open shells support automatic refresh,
|
|
305
|
+
// keep at least 11 hours for every new shell (about an hour of cache reuse).
|
|
306
|
+
// Already-open shells keep their old, still-valid grants until expiry.
|
|
307
|
+
// Also remint after a backwards clock jump rather than exceed Ghost's cap.
|
|
308
|
+
if (this.contentGrantExpiresAtSeconds - nowSeconds < MIN_CONTENT_GRANT_REMAINING_SECONDS ||
|
|
309
|
+
this.contentGrantExpiresAtSeconds > latestExpiry) {
|
|
310
|
+
this.contentGrantExpiresAtSeconds = latestExpiry;
|
|
311
|
+
}
|
|
312
|
+
return this.contentGrantExpiresAtSeconds;
|
|
313
|
+
}
|
|
300
314
|
/**
|
|
301
315
|
* Origin a browser should use for a local content role.
|
|
302
316
|
*
|