castle-web-cli 0.4.91 → 0.4.93
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 +141 -22
- package/dist/api.d.ts +8 -0
- package/dist/api.js +10 -0
- package/dist/bundle.js +2 -2
- package/dist/get-deck.d.ts +1 -1
- package/dist/get-deck.js +93 -22
- package/dist/ide.js +123 -15
- package/dist/imports.d.ts +22 -0
- package/dist/imports.js +549 -0
- package/dist/index.js +45 -15
- package/dist/init.js +169 -1
- package/dist/install.d.ts +1 -1
- package/dist/install.js +14 -1
- package/dist/metering.d.ts +1 -0
- package/dist/metering.js +1 -1
- package/dist/native/loop.js +1 -0
- package/dist/native/openrouter.d.ts +1 -0
- package/dist/native/openrouter.js +13 -6
- package/dist/native/types.d.ts +1 -0
- package/dist/normalize.js +4 -0
- package/dist/openrouter-catalog.d.ts +3 -1
- package/dist/openrouter-catalog.js +15 -10
- package/dist/save-deck.d.ts +2 -0
- package/dist/save-deck.js +25 -19
- package/dist/serve.js +2 -2
- package/dist/shell/assets/{index-DSIr52Kl.css → index-CWNH9QiB.css} +1 -1
- package/dist/shell/assets/{index-BFCG4tLs.js → index-_C2BvstY.js} +21 -21
- package/dist/shell/index.html +2 -2
- package/dist/vitePlugins.d.ts +1 -0
- package/dist/vitePlugins.js +33 -0
- package/kits/basic-2d/CLAUDE.md +20 -0
- package/kits/basic-2d/behaviors/Sprite.jsx +6 -1
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +14 -8
- package/kits/basic-2d/editors/SceneEditor.jsx +39 -6
- package/kits/basic-2d/editors/behaviorRegistry.js +8 -2
- package/kits/basic-2d/engine/behaviorExtensions.js +5 -1
- package/kits/basic-2d/engine/blueprint.js +39 -3
- package/kits/basic-2d/engine/files.js +26 -5
- package/kits/basic-2d/engine/scene.js +14 -3
- package/kits/basic-2d/engine/systemRegistry.js +5 -1
- 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/dist/pull.d.ts +0 -4
- package/dist/pull.js +0 -119
- package/kits/physics-2d/engine/behaviorExtensions.js +0 -28
- package/kits/physics-2d/physics/extensions/collider.js +0 -15
package/dist/init.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { COMMON_INSTRUCTIONS } from "./commonInstructions.js";
|
|
4
4
|
import { installDeps } from "./install.js";
|
|
5
|
+
import { IMPORTS_DIR, lockImportTree } from "./imports.js";
|
|
5
6
|
import { getCliEntryPath, getKitsDir, getRepoRoot, getSdkPackagePath, toPosixPath, } from "./localPaths.js";
|
|
6
7
|
import { serve } from "./serve.js";
|
|
7
8
|
const INDEX_HTML = `<!DOCTYPE html>
|
|
@@ -40,6 +41,9 @@ const PUBLISHED_SDK_VERSION = "0.4.10";
|
|
|
40
41
|
// (the kit ships a config-only one with the editor layout / file filters), but
|
|
41
42
|
// `scaffoldFromKit` strips any identity fields off it first -- a fresh deck has
|
|
42
43
|
// no deckId until its first save-deck, which owns those fields.
|
|
44
|
+
// The account the first-party kits are (to be) published under, so a kit
|
|
45
|
+
// imported from the CLI's copy is named the same as one fetched from the server.
|
|
46
|
+
const KIT_AUTHOR = "castle";
|
|
43
47
|
const KIT_COPY_EXCLUDE = new Set([
|
|
44
48
|
"node_modules",
|
|
45
49
|
".castle",
|
|
@@ -181,6 +185,170 @@ function scaffoldBare(projectDir) {
|
|
|
181
185
|
}
|
|
182
186
|
// Copy a framework kit from kits/<kit>/ into the new deck dir, dropping
|
|
183
187
|
// build/dependency junk and castle.json.
|
|
188
|
+
// Shared by both scaffold paths: resolve the kit directory or exit with the
|
|
189
|
+
// list of kits that do exist.
|
|
190
|
+
function requireKitDir(kit) {
|
|
191
|
+
const kitDir = path.join(getKitsDir(), kit);
|
|
192
|
+
if (fs.existsSync(kitDir) && fs.statSync(kitDir).isDirectory())
|
|
193
|
+
return kitDir;
|
|
194
|
+
console.error(`Kit "${kit}" not found at ${kitDir}.`);
|
|
195
|
+
console.error("Available kits:");
|
|
196
|
+
try {
|
|
197
|
+
const kits = fs
|
|
198
|
+
.readdirSync(getKitsDir())
|
|
199
|
+
.filter((name) => fs.statSync(path.join(getKitsDir(), name)).isDirectory());
|
|
200
|
+
if (kits.length)
|
|
201
|
+
for (const name of kits)
|
|
202
|
+
console.error(` ${name}`);
|
|
203
|
+
else
|
|
204
|
+
console.error(" (none)");
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
console.error(" (none — kits/ directory is missing)");
|
|
208
|
+
}
|
|
209
|
+
console.error("Or use `--kit none` for a bare code-only deck.");
|
|
210
|
+
return process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
function readJsonFile(file) {
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function writeJsonFile(file, value) {
|
|
221
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
222
|
+
}
|
|
223
|
+
function getCliVersion() {
|
|
224
|
+
const pkg = readJsonFile(path.join(getKitsDir(), "..", "package.json"));
|
|
225
|
+
return typeof pkg?.version === "string" ? pkg.version : "0.0.0";
|
|
226
|
+
}
|
|
227
|
+
// The deck's entry point is the KIT's -- the deck has no engine code of its own
|
|
228
|
+
// -- so index.html points into the import. Everything the kit's main.jsx imports
|
|
229
|
+
// is relative to itself, so it needs no rewriting to run from there.
|
|
230
|
+
function writeDeckIndexHtml(projectDir, alias, title) {
|
|
231
|
+
fs.writeFileSync(path.join(projectDir, "index.html"), `<!doctype html>
|
|
232
|
+
<html>
|
|
233
|
+
<head>
|
|
234
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
|
235
|
+
<meta name="color-scheme" content="dark" />
|
|
236
|
+
<title>${title}</title>
|
|
237
|
+
<!-- Declare the dark scheme before the JS-imported CSS loads, so the frame
|
|
238
|
+
never paints light first (Safari won't repaint a composited iframe on a
|
|
239
|
+
color-scheme change). -->
|
|
240
|
+
<style>
|
|
241
|
+
html {
|
|
242
|
+
color-scheme: dark;
|
|
243
|
+
background: #0a0a0a;
|
|
244
|
+
}
|
|
245
|
+
</style>
|
|
246
|
+
</head>
|
|
247
|
+
<body>
|
|
248
|
+
<div id="root"></div>
|
|
249
|
+
<!-- Engine and editors come from the imported kit; this deck holds its own
|
|
250
|
+
scenes, drawings and behaviors. -->
|
|
251
|
+
<script type="module" src="/${IMPORTS_DIR}/${alias}/main.jsx"></script>
|
|
252
|
+
</body>
|
|
253
|
+
</html>
|
|
254
|
+
`, "utf8");
|
|
255
|
+
}
|
|
256
|
+
// The deck starts with its own copy of the kit's starter scene, so there is
|
|
257
|
+
// something to open and edit on the first run. Refs inside it are rewritten to
|
|
258
|
+
// point at the kit's copies of what they name (the scene is the deck's now, but
|
|
259
|
+
// the blueprint and art it places still live in the import).
|
|
260
|
+
function writeStarterScene(projectDir, kitDir, alias) {
|
|
261
|
+
const scene = readJsonFile(path.join(kitDir, "scenes", "main.scene"));
|
|
262
|
+
const prefix = `${IMPORTS_DIR}/${alias}/`;
|
|
263
|
+
const rewrite = (value) => {
|
|
264
|
+
if (typeof value === "string") {
|
|
265
|
+
return fs.existsSync(path.join(kitDir, value)) ? prefix + value : value;
|
|
266
|
+
}
|
|
267
|
+
if (Array.isArray(value))
|
|
268
|
+
return value.map(rewrite);
|
|
269
|
+
if (value && typeof value === "object") {
|
|
270
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, rewrite(v)]));
|
|
271
|
+
}
|
|
272
|
+
return value;
|
|
273
|
+
};
|
|
274
|
+
fs.mkdirSync(path.join(projectDir, "scenes"), { recursive: true });
|
|
275
|
+
writeJsonFile(path.join(projectDir, "scenes", "main.scene"), scene ? rewrite(scene) : { name: "Main", actors: [] });
|
|
276
|
+
}
|
|
277
|
+
function makeImportedKitClaudeMd(alias) {
|
|
278
|
+
return `# Castle deck
|
|
279
|
+
|
|
280
|
+
The engine, editors and built-in behaviors come from the **${alias}** kit, imported
|
|
281
|
+
at \`${IMPORTS_DIR}/${alias}/\` and read-only. Read \`${IMPORTS_DIR}/${alias}/CLAUDE.md\`
|
|
282
|
+
for the authoring guide: how scenes, blueprints, drawings and behaviors work.
|
|
283
|
+
|
|
284
|
+
This deck holds only its own content -- \`scenes/\`, \`drawings/\`,
|
|
285
|
+
\`blueprints/\`, \`behaviors/\` -- plus \`index.html\` (which points at the kit's
|
|
286
|
+
entry point) and \`castle.json\` (editor config and the import pin).
|
|
287
|
+
|
|
288
|
+
Do not edit anything under \`${IMPORTS_DIR}/\`: those files belong to the deck they
|
|
289
|
+
came from, and are replaced wholesale when the import is updated. To change how
|
|
290
|
+
something in the kit behaves, define your own behavior of the same name in
|
|
291
|
+
\`behaviors/\` -- the deck's own files win over an import's.
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
// Scaffold a deck that IMPORTS its kit instead of copying it. The kit's files
|
|
295
|
+
// land in `imports/<kit>/` (read-only, like any dependency) and the deck itself
|
|
296
|
+
// holds only what is genuinely its own: an entry point, its config, and its
|
|
297
|
+
// starting content. A kit fix then reaches the deck by re-fetching the import,
|
|
298
|
+
// rather than being frozen into the deck at scaffold time -- which is what
|
|
299
|
+
// copying the kit in meant.
|
|
300
|
+
//
|
|
301
|
+
// The kit comes from the copy shipped with this CLI, so `init` works offline and
|
|
302
|
+
// needs no account. The pin records that origin; once kits are published as
|
|
303
|
+
// decks it becomes a deckId + version and the files are fetched instead, with
|
|
304
|
+
// the same deck shape either way.
|
|
305
|
+
function scaffoldFromKitImport(kit, projectDir) {
|
|
306
|
+
const kitDir = requireKitDir(kit);
|
|
307
|
+
// Qualified like any other import (`<author>.<deck>`), and qualified NOW even
|
|
308
|
+
// though this copy is the CLI's: the alias is baked into every ref the deck
|
|
309
|
+
// writes, so it can't change later without rewriting them. Publishing the
|
|
310
|
+
// kits under the castle account turns the pin into a deckId; the name it is
|
|
311
|
+
// already known by stays put.
|
|
312
|
+
const alias = `${KIT_AUTHOR}.${kit}`;
|
|
313
|
+
const importDir = path.join(projectDir, IMPORTS_DIR, alias);
|
|
314
|
+
fs.mkdirSync(importDir, { recursive: true });
|
|
315
|
+
fs.cpSync(kitDir, importDir, {
|
|
316
|
+
recursive: true,
|
|
317
|
+
verbatimSymlinks: true,
|
|
318
|
+
filter: (src) => src === kitDir || !KIT_COPY_EXCLUDE.has(path.basename(src)),
|
|
319
|
+
});
|
|
320
|
+
const kitConfig = readJsonFile(path.join(kitDir, "castle.json")) ?? {};
|
|
321
|
+
const kitPkg = readJsonFile(path.join(kitDir, "package.json")) ?? {};
|
|
322
|
+
writeDeckIndexHtml(projectDir, alias, String(kitConfig.title ?? kit));
|
|
323
|
+
writeStarterScene(projectDir, kitDir, alias);
|
|
324
|
+
// The deck's own castle.json: the kit's editor config (panel layout, file
|
|
325
|
+
// filters) as a starting point -- it is the deck's to edit from here -- plus
|
|
326
|
+
// the import pin.
|
|
327
|
+
writeJsonFile(path.join(projectDir, "castle.json"), {
|
|
328
|
+
...(kitConfig.editor ? { editor: kitConfig.editor } : {}),
|
|
329
|
+
imports: { [alias]: { source: "builtin", kit, version: getCliVersion() } },
|
|
330
|
+
});
|
|
331
|
+
// The kit's code runs from THIS deck's node_modules, so its dependencies are
|
|
332
|
+
// declared here (see syncImportDependencies, which does the same for imports
|
|
333
|
+
// added later).
|
|
334
|
+
const { sdkRef } = resolveScaffoldRefs();
|
|
335
|
+
const dependencies = {};
|
|
336
|
+
for (const [name, range] of Object.entries(kitPkg.dependencies ?? {})) {
|
|
337
|
+
dependencies[name] =
|
|
338
|
+
name === "castle-web-sdk" && String(range).startsWith("file:")
|
|
339
|
+
? sdkRef
|
|
340
|
+
: String(range);
|
|
341
|
+
}
|
|
342
|
+
writeJsonFile(path.join(projectDir, "package.json"), {
|
|
343
|
+
name: path.basename(projectDir),
|
|
344
|
+
private: true,
|
|
345
|
+
type: "module",
|
|
346
|
+
dependencies,
|
|
347
|
+
});
|
|
348
|
+
fs.writeFileSync(path.join(projectDir, "CLAUDE.md"), makeImportedKitClaudeMd(alias));
|
|
349
|
+
appendCommonInstructions(projectDir);
|
|
350
|
+
lockImportTree(importDir);
|
|
351
|
+
}
|
|
184
352
|
function scaffoldFromKit(kit, projectDir) {
|
|
185
353
|
const kitDir = path.join(getKitsDir(), kit);
|
|
186
354
|
if (!fs.existsSync(kitDir) || !fs.statSync(kitDir).isDirectory()) {
|
|
@@ -278,7 +446,7 @@ export async function init(dir, opts = {}) {
|
|
|
278
446
|
scaffoldBare(projectDir);
|
|
279
447
|
}
|
|
280
448
|
else {
|
|
281
|
-
|
|
449
|
+
scaffoldFromKitImport(kit, projectDir);
|
|
282
450
|
}
|
|
283
451
|
console.log(`Created project in ${projectDir}/${bare ? "" : ` (from kit "${kit}")`}`);
|
|
284
452
|
// Always install deps so the deck is ready to serve/edit immediately.
|
package/dist/install.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare function installDeps(projectDir: string, frozen: boolean): void;
|
|
2
|
-
export declare function install(dir: string): void
|
|
2
|
+
export declare function install(dir: string): Promise<void>;
|
package/dist/install.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
+
import { restoreMissingImports, syncImportDependencies } from "./imports.js";
|
|
4
5
|
function hasPnpm() {
|
|
5
6
|
try {
|
|
6
7
|
execSync("pnpm --version", { stdio: "ignore" });
|
|
@@ -53,12 +54,24 @@ export function installDeps(projectDir, frozen) {
|
|
|
53
54
|
// `castle-web install <dir>`: put node_modules in place for a deck that already has
|
|
54
55
|
// its source -- one fetched with `get-deck`, or one whose node_modules was dropped
|
|
55
56
|
// (the cloud hosts treat it as regenerable and don't snapshot it).
|
|
56
|
-
export function install(dir) {
|
|
57
|
+
export async function install(dir) {
|
|
57
58
|
const projectDir = path.resolve(dir);
|
|
58
59
|
if (!fs.existsSync(path.join(projectDir, "package.json"))) {
|
|
59
60
|
console.error(`No package.json in ${projectDir}.`);
|
|
60
61
|
process.exit(1);
|
|
61
62
|
}
|
|
63
|
+
// A fetched deck carries its import pins but not the files (imports/ is left
|
|
64
|
+
// out of the archive), so put them back before anything depends on them.
|
|
65
|
+
const restored = await restoreMissingImports(projectDir);
|
|
66
|
+
for (const alias of restored)
|
|
67
|
+
console.log(`Restored import ${alias}`);
|
|
68
|
+
// Imports declare packages the deck has to install (see syncImportDependencies).
|
|
69
|
+
// Re-done here, not just at add-import time, so a deck fetched with get-deck --
|
|
70
|
+
// which brings castle.json's pins but not the dependency files -- still ends up
|
|
71
|
+
// with everything its imports need once they are back.
|
|
72
|
+
const added = syncImportDependencies(projectDir);
|
|
73
|
+
for (const dep of added)
|
|
74
|
+
console.log(`Added dependency ${dep} from an import`);
|
|
62
75
|
installDeps(projectDir, fs.existsSync(path.join(projectDir, "pnpm-lock.yaml")));
|
|
63
76
|
console.log(`Installed deps in ${projectDir}`);
|
|
64
77
|
}
|
package/dist/metering.d.ts
CHANGED
package/dist/metering.js
CHANGED
|
@@ -49,7 +49,7 @@ function deckIdFor(deckDir) {
|
|
|
49
49
|
return parsed.deckId.replace(/[\r\n]/g, "") || null;
|
|
50
50
|
}
|
|
51
51
|
export function meteringHeaders(opts) {
|
|
52
|
-
if (!proxyInjected(opts.route))
|
|
52
|
+
if (opts.direct || !proxyInjected(opts.route))
|
|
53
53
|
return {};
|
|
54
54
|
const deckId = deckIdFor(opts.deckDir);
|
|
55
55
|
return {
|
package/dist/native/loop.js
CHANGED
|
@@ -599,6 +599,7 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
599
599
|
const iterationStartedAt = Date.now();
|
|
600
600
|
const streamResult = await streamChatCompletion({
|
|
601
601
|
apiKey: opts.apiKey,
|
|
602
|
+
baseUrl: opts.baseUrl,
|
|
602
603
|
model: opts.model,
|
|
603
604
|
messages,
|
|
604
605
|
tools: toolSchemas,
|
|
@@ -33,6 +33,7 @@ export type ORRoutingMode = "balanced" | "nitro" | "exacto" | "floor";
|
|
|
33
33
|
export declare function applyRoutingMode(model: string, routing?: ORRoutingMode): string;
|
|
34
34
|
export interface StreamChatOpts {
|
|
35
35
|
apiKey: string;
|
|
36
|
+
baseUrl?: string;
|
|
36
37
|
model: string;
|
|
37
38
|
messages: ORMessage[];
|
|
38
39
|
tools?: unknown[];
|
|
@@ -19,11 +19,13 @@ import { failureForStatus } from "../agent-failures.js";
|
|
|
19
19
|
// a local fake server instead of the real API -- read per-call (not hoisted
|
|
20
20
|
// to a module-level const) so a test process that imports this module once
|
|
21
21
|
// can still point successive runAgentNative calls at different fake servers.
|
|
22
|
-
function openrouterUrl() {
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
function openrouterUrl(base) {
|
|
23
|
+
// A user's OWN OpenRouter key passes `base` (openrouter.ai) so the run bypasses
|
|
24
|
+
// the proxy. Otherwise, in a Castle sandbox the host injects OPENROUTER_BASE_URL
|
|
25
|
+
// pointing at the per-host llm-proxy (holds the real key, meters usage); else
|
|
26
|
+
// talk to openrouter.ai directly. The test override wins over all of it.
|
|
25
27
|
return (process.env.CASTLE_OPENROUTER_URL ||
|
|
26
|
-
`${process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1"}/chat/completions`);
|
|
28
|
+
`${base || process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1"}/chat/completions`);
|
|
27
29
|
}
|
|
28
30
|
const DEFAULT_MAX_RETRIES = 2; // -> 3 total connect attempts
|
|
29
31
|
const RETRY_BASE_MS = 500;
|
|
@@ -91,7 +93,7 @@ async function connectWithRetry(init, opts) {
|
|
|
91
93
|
}
|
|
92
94
|
let res;
|
|
93
95
|
try {
|
|
94
|
-
res = await fetch(
|
|
96
|
+
res = await fetch(opts.url, init);
|
|
95
97
|
}
|
|
96
98
|
catch (err) {
|
|
97
99
|
if (err instanceof Error && err.name === "AbortError")
|
|
@@ -253,7 +255,12 @@ export async function streamChatCompletion(opts) {
|
|
|
253
255
|
},
|
|
254
256
|
body: JSON.stringify(body),
|
|
255
257
|
signal: opts.signal,
|
|
256
|
-
}, {
|
|
258
|
+
}, {
|
|
259
|
+
url: openrouterUrl(opts.baseUrl),
|
|
260
|
+
signal: opts.signal,
|
|
261
|
+
maxRetries: opts.maxRetries,
|
|
262
|
+
onRetry: opts.onRetry,
|
|
263
|
+
});
|
|
257
264
|
}
|
|
258
265
|
catch (err) {
|
|
259
266
|
if (err instanceof Error && err.name === "AbortError") {
|
package/dist/native/types.d.ts
CHANGED
package/dist/normalize.js
CHANGED
|
@@ -60,7 +60,11 @@ export function normalizeDeckPackageJson(projectDir) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
if (changed) {
|
|
63
|
+
const prev = fs.statSync(pkgPath);
|
|
63
64
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
65
|
+
// Machine wiring, not an edit: keep the old mtime so get-deck's staleness
|
|
66
|
+
// guard doesn't read a freshly fetched deck as locally modified.
|
|
67
|
+
fs.utimesSync(pkgPath, prev.atime, prev.mtime);
|
|
64
68
|
}
|
|
65
69
|
return changed;
|
|
66
70
|
}
|
|
@@ -23,6 +23,8 @@ export type KeyCheck = {
|
|
|
23
23
|
} | {
|
|
24
24
|
status: "unavailable";
|
|
25
25
|
};
|
|
26
|
-
export declare function checkOpenrouterKey(apiKey: string
|
|
26
|
+
export declare function checkOpenrouterKey(apiKey: string, opts?: {
|
|
27
|
+
direct?: boolean;
|
|
28
|
+
}): Promise<KeyCheck>;
|
|
27
29
|
export declare function checkOpenrouterModel(slug: string): Promise<ModelCheck>;
|
|
28
30
|
export declare function openrouterCatalogEntry(slug: string): Promise<CatalogEntry | null>;
|
|
@@ -28,12 +28,17 @@ function modelsUrl() {
|
|
|
28
28
|
return (process.env.CASTLE_OPENROUTER_MODELS_URL ??
|
|
29
29
|
"https://openrouter.ai/api/v1/models");
|
|
30
30
|
}
|
|
31
|
-
// The key check sends the credential, so
|
|
32
|
-
// (OPENROUTER_BASE_URL) — validating the real key the proxy swaps in.
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
// The key check sends the credential, so by default (proxy mode) it goes through the llm-proxy
|
|
32
|
+
// (OPENROUTER_BASE_URL) — validating the real key the proxy swaps in. `direct` (a user's OWN
|
|
33
|
+
// OpenRouter key, which the proxy would reject) validates against openrouter.ai instead.
|
|
34
|
+
// modelsUrl above is a public GET and stays on openrouter.ai (the proxy requires a token).
|
|
35
|
+
function keyUrl(direct) {
|
|
36
|
+
if (process.env.CASTLE_OPENROUTER_KEY_URL)
|
|
37
|
+
return process.env.CASTLE_OPENROUTER_KEY_URL;
|
|
38
|
+
const base = direct
|
|
39
|
+
? "https://openrouter.ai/api/v1"
|
|
40
|
+
: process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1";
|
|
41
|
+
return `${base}/key`;
|
|
37
42
|
}
|
|
38
43
|
function cachePath() {
|
|
39
44
|
return (process.env.CASTLE_OPENROUTER_CATALOG_CACHE ??
|
|
@@ -220,11 +225,11 @@ function keyHandle(apiKey) {
|
|
|
220
225
|
h = (Math.imul(h, 31) + apiKey.charCodeAt(i)) | 0;
|
|
221
226
|
return `k${(h >>> 0).toString(36)}`;
|
|
222
227
|
}
|
|
223
|
-
async function fetchKeyCheck(apiKey) {
|
|
228
|
+
async function fetchKeyCheck(apiKey, direct) {
|
|
224
229
|
const controller = new AbortController();
|
|
225
230
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
226
231
|
try {
|
|
227
|
-
const res = await fetch(keyUrl(), {
|
|
232
|
+
const res = await fetch(keyUrl(direct), {
|
|
228
233
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
229
234
|
signal: controller.signal,
|
|
230
235
|
});
|
|
@@ -249,7 +254,7 @@ async function fetchKeyCheck(apiKey) {
|
|
|
249
254
|
clearTimeout(timer);
|
|
250
255
|
}
|
|
251
256
|
}
|
|
252
|
-
export async function checkOpenrouterKey(apiKey) {
|
|
257
|
+
export async function checkOpenrouterKey(apiKey, opts) {
|
|
253
258
|
if (!apiKey)
|
|
254
259
|
return { status: "bad-key" };
|
|
255
260
|
const handle = keyHandle(apiKey);
|
|
@@ -260,7 +265,7 @@ export async function checkOpenrouterKey(apiKey) {
|
|
|
260
265
|
const existing = keyInflight.get(handle);
|
|
261
266
|
if (existing)
|
|
262
267
|
return existing;
|
|
263
|
-
const p = fetchKeyCheck(apiKey)
|
|
268
|
+
const p = fetchKeyCheck(apiKey, opts?.direct)
|
|
264
269
|
.then((result) => {
|
|
265
270
|
// An "unavailable" verdict is deliberately NOT cached -- it means the
|
|
266
271
|
// network hiccuped, and caching it would suppress validation for the
|
package/dist/save-deck.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare const SOURCE_ARCHIVE_EXCLUDES: string[];
|
|
2
|
+
export declare function runTar(args: string[]): Promise<void>;
|
|
1
3
|
export declare function archiveSource(projectDir: string): Promise<Buffer>;
|
|
2
4
|
export type SaveVisibility = 'unlisted' | 'private';
|
|
3
5
|
export interface SaveDeckOptions {
|
package/dist/save-deck.js
CHANGED
|
@@ -6,38 +6,44 @@ import { nanoid } from 'nanoid';
|
|
|
6
6
|
import * as api from './api.js';
|
|
7
7
|
import * as config from './config.js';
|
|
8
8
|
import { bundleProject } from './bundle.js';
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// `imports/` is derived state like node_modules: a dependency's files belong to
|
|
10
|
+
// the deck that published them, and castle.json's import pins are enough to fetch
|
|
11
|
+
// them again -- so they neither bloat this deck's archive nor get republished as
|
|
12
|
+
// part of it. (Being on this list also means `get-deck` leaves an existing
|
|
13
|
+
// `imports/` in place when it refreshes a deck.)
|
|
14
|
+
export const SOURCE_ARCHIVE_EXCLUDES = ['node_modules', 'dist', '.castle', '.git', 'imports'];
|
|
15
|
+
export function runTar(args) {
|
|
11
16
|
return new Promise((resolve, reject) => {
|
|
12
|
-
const tmpFile = path.join(os.tmpdir(), `castle-source-${nanoid(8)}.tar.gz`);
|
|
13
|
-
const args = ['-czf', tmpFile];
|
|
14
|
-
for (const ex of SOURCE_ARCHIVE_EXCLUDES)
|
|
15
|
-
args.push(`--exclude=./${ex}`);
|
|
16
|
-
args.push('-C', projectDir, '.');
|
|
17
17
|
const child = spawn('tar', args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
18
18
|
let stderr = '';
|
|
19
19
|
child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
20
20
|
child.on('error', reject);
|
|
21
21
|
child.on('close', (code) => {
|
|
22
22
|
if (code !== 0) {
|
|
23
|
-
try {
|
|
24
|
-
fs.unlinkSync(tmpFile);
|
|
25
|
-
}
|
|
26
|
-
catch { /* nothing to clean */ }
|
|
27
23
|
reject(new Error(`tar exited with code ${code}: ${stderr}`));
|
|
28
24
|
return;
|
|
29
25
|
}
|
|
30
|
-
|
|
31
|
-
const buf = fs.readFileSync(tmpFile);
|
|
32
|
-
fs.unlinkSync(tmpFile);
|
|
33
|
-
resolve(buf);
|
|
34
|
-
}
|
|
35
|
-
catch (e) {
|
|
36
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
37
|
-
}
|
|
26
|
+
resolve();
|
|
38
27
|
});
|
|
39
28
|
});
|
|
40
29
|
}
|
|
30
|
+
export async function archiveSource(projectDir) {
|
|
31
|
+
const tmpFile = path.join(os.tmpdir(), `castle-source-${nanoid(8)}.tar.gz`);
|
|
32
|
+
const args = ['-czf', tmpFile];
|
|
33
|
+
for (const ex of SOURCE_ARCHIVE_EXCLUDES)
|
|
34
|
+
args.push(`--exclude=./${ex}`);
|
|
35
|
+
args.push('-C', projectDir, '.');
|
|
36
|
+
try {
|
|
37
|
+
await runTar(args);
|
|
38
|
+
return fs.readFileSync(tmpFile);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
try {
|
|
42
|
+
fs.unlinkSync(tmpFile);
|
|
43
|
+
}
|
|
44
|
+
catch { /* nothing to clean */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
41
47
|
async function uploadSource(projectDir, deckId) {
|
|
42
48
|
const archive = await archiveSource(projectDir);
|
|
43
49
|
const sizeKB = archive.length / 1024;
|
package/dist/serve.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createServer } from 'vite';
|
|
|
6
6
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
7
|
import { createIdeServer } from './ide.js';
|
|
8
8
|
import { createAgentServer } from './agent.js';
|
|
9
|
-
import { sceneFilesPlugin } from './vitePlugins.js';
|
|
9
|
+
import { sceneFilesPlugin, importsAliasPlugin } from './vitePlugins.js';
|
|
10
10
|
import { installFilesChangedWatcher } from './filesChanged.js';
|
|
11
11
|
import * as config from './config.js';
|
|
12
12
|
import { graphql as castleGraphql } from './api.js';
|
|
@@ -271,7 +271,7 @@ export async function serve(dir, options = {}) {
|
|
|
271
271
|
process.on('exit', () => agentServer.shutdown());
|
|
272
272
|
const vite = await createServer({
|
|
273
273
|
root: projectDir,
|
|
274
|
-
plugins: [castlePlugin(wsPort, ideServer, agentServer), sceneFilesPlugin()],
|
|
274
|
+
plugins: [castlePlugin(wsPort, ideServer, agentServer), importsAliasPlugin(), sceneFilesPlugin()],
|
|
275
275
|
server: {
|
|
276
276
|
port,
|
|
277
277
|
strictPort: true,
|