castle-web-cli 0.4.82 → 0.4.84

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 (37) hide show
  1. package/dist/agent-failures.d.ts +17 -0
  2. package/dist/agent-failures.js +151 -0
  3. package/dist/agent.d.ts +27 -0
  4. package/dist/agent.js +614 -57
  5. package/dist/ide.js +150 -1
  6. package/dist/native/loop.js +40 -1
  7. package/dist/native/openrouter.d.ts +12 -1
  8. package/dist/native/openrouter.js +45 -2
  9. package/dist/native/types.d.ts +6 -0
  10. package/dist/native/types.js +0 -38
  11. package/dist/openrouter-catalog.d.ts +28 -0
  12. package/dist/openrouter-catalog.js +299 -0
  13. package/dist/shell/assets/index-BOgm5T3W.js +144 -0
  14. package/dist/shell/assets/index-DonnH--m.css +1 -0
  15. package/dist/shell/index.html +2 -2
  16. package/dist/shell/operator.png +0 -0
  17. package/kits/basic-2d/CLAUDE.md +27 -23
  18. package/kits/basic-2d/behaviors/Collider.jsx +24 -30
  19. package/kits/basic-2d/behaviors/Layout.jsx +9 -6
  20. package/kits/basic-2d/behaviors/Sprite.jsx +137 -7
  21. package/kits/basic-2d/blueprints/cauldron.scene +3 -5
  22. package/kits/basic-2d/editors/BlueprintLibrary.jsx +11 -11
  23. package/kits/basic-2d/editors/SceneEditor.jsx +212 -50
  24. package/kits/basic-2d/editors/SelectionOverlay.jsx +73 -54
  25. package/kits/basic-2d/editors/inspectorSheet.js +5 -1
  26. package/kits/basic-2d/engine/ScenePlayer.jsx +98 -7
  27. package/kits/basic-2d/engine/autoInspector.jsx +26 -7
  28. package/kits/basic-2d/engine/blueprint.js +35 -8
  29. package/kits/basic-2d/engine/collider.js +146 -0
  30. package/kits/basic-2d/engine/scene.js +53 -30
  31. package/kits/basic-2d/engine/spriteGeometry.js +32 -0
  32. package/kits/basic-2d/engine/ui.jsx +89 -30
  33. package/kits/basic-2d/engine/ui.module.css +157 -53
  34. package/kits/basic-2d/scenes/main.scene +3 -3
  35. package/package.json +2 -1
  36. package/dist/shell/assets/index-ByhgiJoP.js +0 -141
  37. package/dist/shell/assets/index-D6hM_VlW.css +0 -1
package/dist/ide.js CHANGED
@@ -268,6 +268,136 @@ function handleFilesWrite(deckDir, req, res) {
268
268
  }
269
269
  })();
270
270
  }
271
+ // Add `<rel>/**` to the deck's editor.visiblePaths so files created in a
272
+ // newly-made folder show in the curated Files tree. Only touches a deck that is
273
+ // ALREADY curated (non-empty visiblePaths) -- when visiblePaths is empty
274
+ // everything is visible, and adding a glob would wrongly start hiding things.
275
+ // No-op if an existing glob already covers the folder. Returns whether it wrote.
276
+ function ensureVisiblePath(deckDir, rel) {
277
+ const file = path.join(deckDir, "castle.json");
278
+ let data;
279
+ try {
280
+ data = JSON.parse(fs.readFileSync(file, "utf8"));
281
+ }
282
+ catch {
283
+ return false; // no castle.json yet (deck never saved) -> treat as not curated
284
+ }
285
+ const visible = data.editor && Array.isArray(data.editor.visiblePaths)
286
+ ? data.editor.visiblePaths.filter((v) => typeof v === "string")
287
+ : null;
288
+ if (!visible || visible.length === 0)
289
+ return false; // not curated -> all visible
290
+ const glob = `${rel}/**`;
291
+ if (visible.includes(glob))
292
+ return false;
293
+ if (picomatch(visible)(`${rel}/__probe__`))
294
+ return false; // already covered
295
+ visible.push(glob);
296
+ data.editor.visiblePaths = visible;
297
+ try {
298
+ fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`, "utf8");
299
+ return true;
300
+ }
301
+ catch {
302
+ return false;
303
+ }
304
+ }
305
+ function handleFilesMkdir(deckDir, req, res) {
306
+ void (async () => {
307
+ let body;
308
+ try {
309
+ body = JSON.parse(await readRequestBody(req));
310
+ }
311
+ catch {
312
+ return sendJson(res, 400, { error: "Invalid JSON body." });
313
+ }
314
+ const resolved = resolveDeckPath(deckDir, body.path);
315
+ if (!resolved.ok)
316
+ return sendJson(res, 400, { error: resolved.error });
317
+ try {
318
+ fs.mkdirSync(resolved.abs, { recursive: true });
319
+ }
320
+ catch (err) {
321
+ const message = err instanceof Error ? err.message : String(err);
322
+ return sendJson(res, 500, { error: `Could not create folder ${resolved.rel}: ${message}` });
323
+ }
324
+ const visiblePathAdded = ensureVisiblePath(deckDir, resolved.rel);
325
+ sendJson(res, 200, { ok: true, path: resolved.rel, visiblePathAdded });
326
+ })();
327
+ }
328
+ // True when two paths resolve to the same underlying file (same inode+device) --
329
+ // e.g. the source and target of a case-only rename on a case-insensitive FS.
330
+ function isSameFile(a, b) {
331
+ try {
332
+ const sa = fs.statSync(a);
333
+ const sb = fs.statSync(b);
334
+ return sa.ino === sb.ino && sa.dev === sb.dev;
335
+ }
336
+ catch {
337
+ return false;
338
+ }
339
+ }
340
+ function handleFilesRename(deckDir, req, res) {
341
+ void (async () => {
342
+ let body;
343
+ try {
344
+ body = JSON.parse(await readRequestBody(req));
345
+ }
346
+ catch {
347
+ return sendJson(res, 400, { error: "Invalid JSON body." });
348
+ }
349
+ const from = resolveDeckPath(deckDir, body.from);
350
+ if (!from.ok)
351
+ return sendJson(res, 400, { error: from.error });
352
+ const to = resolveDeckPath(deckDir, body.to);
353
+ if (!to.ok)
354
+ return sendJson(res, 400, { error: to.error });
355
+ if (!fs.existsSync(from.abs)) {
356
+ return sendJson(res, 404, { error: `Not found: ${from.rel}` });
357
+ }
358
+ // Block a collision with a DIFFERENT existing file. On a case-insensitive
359
+ // filesystem (default on macOS/Windows) `to` can "exist" only because it is
360
+ // `from` under a different case -- a case-only rename like bounce.jsx ->
361
+ // Bounce.jsx. Allow that by treating same-inode as not-a-collision.
362
+ if (from.abs !== to.abs && fs.existsSync(to.abs) && !isSameFile(from.abs, to.abs)) {
363
+ return sendJson(res, 409, { error: `Already exists: ${to.rel}` });
364
+ }
365
+ try {
366
+ fs.mkdirSync(path.dirname(to.abs), { recursive: true });
367
+ fs.renameSync(from.abs, to.abs);
368
+ sendJson(res, 200, { ok: true, path: to.rel });
369
+ }
370
+ catch (err) {
371
+ const message = err instanceof Error ? err.message : String(err);
372
+ sendJson(res, 500, { error: `Could not rename ${from.rel}: ${message}` });
373
+ }
374
+ })();
375
+ }
376
+ function handleFilesDelete(deckDir, req, res) {
377
+ void (async () => {
378
+ let body;
379
+ try {
380
+ body = JSON.parse(await readRequestBody(req));
381
+ }
382
+ catch {
383
+ return sendJson(res, 400, { error: "Invalid JSON body." });
384
+ }
385
+ const resolved = resolveDeckPath(deckDir, body.path);
386
+ if (!resolved.ok)
387
+ return sendJson(res, 400, { error: resolved.error });
388
+ if (!fs.existsSync(resolved.abs)) {
389
+ return sendJson(res, 404, { error: `Not found: ${resolved.rel}` });
390
+ }
391
+ try {
392
+ fs.rmSync(resolved.abs, { recursive: true, force: true });
393
+ sendJson(res, 200, { ok: true, path: resolved.rel });
394
+ }
395
+ catch (err) {
396
+ const message = err instanceof Error ? err.message : String(err);
397
+ sendJson(res, 500, { error: `Could not delete ${resolved.rel}: ${message}` });
398
+ }
399
+ })();
400
+ }
271
401
  // The builtin Files + code-editor backend: list / read / write deck files and
272
402
  // report kit-owned editor extensions. Paths are deck-relative; resolveDeckPath
273
403
  // rejects traversal and protected dirs.
@@ -286,7 +416,14 @@ function handleFilesApi(deckDir, req, res, reqPath) {
286
416
  return true;
287
417
  }
288
418
  if (action === "list") {
289
- const files = filterDeckFiles(listDeckFiles(deckDir), readEditorConfig(deckDir));
419
+ // `?all=1` returns the unfiltered listing (the "show hidden files & folders"
420
+ // toggle) -- still minus the always-ignored dirs (node_modules/.castle/...),
421
+ // just without the deck's visible/hidden path curation.
422
+ const url = new URL(req.url ?? "/", "http://localhost");
423
+ const listed = listDeckFiles(deckDir);
424
+ const files = url.searchParams.get("all") === "1"
425
+ ? listed
426
+ : filterDeckFiles(listed, readEditorConfig(deckDir));
290
427
  sendJson(res, 200, { files });
291
428
  return true;
292
429
  }
@@ -308,6 +445,18 @@ function handleFilesApi(deckDir, req, res, reqPath) {
308
445
  handleFilesWrite(deckDir, req, res);
309
446
  return true;
310
447
  }
448
+ if (action === "rename") {
449
+ handleFilesRename(deckDir, req, res);
450
+ return true;
451
+ }
452
+ if (action === "delete") {
453
+ handleFilesDelete(deckDir, req, res);
454
+ return true;
455
+ }
456
+ if (action === "mkdir") {
457
+ handleFilesMkdir(deckDir, req, res);
458
+ return true;
459
+ }
311
460
  return sendJson(res, 404, { error: `Unknown files action: ${action}` }), true;
312
461
  }
313
462
  function defaultShell() {
@@ -565,6 +565,10 @@ async function runLoop(opts, toolSchemas, log) {
565
565
  const imageLabels = new Map();
566
566
  let finalText = "";
567
567
  let totalUsage;
568
+ // Whether this run has called ANY tool, across every iteration -- not just
569
+ // the current one. Scoped to the run because the question is "did this task
570
+ // do anything at all", which one iteration can't answer.
571
+ let usedAnyTool = false;
568
572
  // Once our own timeout/external-abort fires, whatever streamChatCompletion
569
573
  // reports is moot -- the abort IS the reason for stopping, so we always
570
574
  // attribute the final error to it (rather than a network error that might
@@ -598,7 +602,11 @@ async function runLoop(opts, toolSchemas, log) {
598
602
  model: opts.model,
599
603
  messages,
600
604
  tools: toolSchemas,
601
- reasoningEffort: REASONING_EFFORT[opts.role],
605
+ // Settings-driven per-role effort; falls back to the built-in table
606
+ // when a caller doesn't supply one (e.g. the QA harness).
607
+ reasoningEffort: opts.reasoningEffort ?? REASONING_EFFORT[opts.role],
608
+ routing: opts.routing,
609
+ providerTier: opts.providerTier,
602
610
  maxTokens: MAX_COMPLETION_TOKENS,
603
611
  signal: controller.signal,
604
612
  onDelta: opts.onDelta,
@@ -631,6 +639,9 @@ async function runLoop(opts, toolSchemas, log) {
631
639
  return {
632
640
  text: finalText,
633
641
  error: streamResult.error ?? "openrouter stream ended without a final response",
642
+ // Present only for an HTTP status streamChat could label; absent for
643
+ // a dropped connection, which stays unclassified on purpose.
644
+ failure: streamResult.failure,
634
645
  usage: totalUsage,
635
646
  playtestFrames: playtestFrameList(playtestFrames),
636
647
  crashed: streamResult.crashed,
@@ -653,12 +664,40 @@ async function runLoop(opts, toolSchemas, log) {
653
664
  : {}),
654
665
  });
655
666
  if (toolCalls.length === 0) {
667
+ // No tool calls is the normal way a run ends -- the model is done and
668
+ // signing off. But a TASK that reaches this on iteration 1, having
669
+ // never called a single tool, did no work: it chatted. That used to
670
+ // return a bare success, so the card finalized "done" at 100% with
671
+ // nothing changed -- the loudest silent failure this backend had.
672
+ //
673
+ // Only tasks: the router legitimately answers in plain text, and a
674
+ // task that used tools and THEN signs off with prose is fine.
675
+ //
676
+ // This narrows the failure class rather than closing it: a task whose
677
+ // only tool calls were reads still counts as "used a tool" here and
678
+ // can still finish having changed nothing. Detecting that needs a
679
+ // notion of which tools mutate, which is a bigger change than this.
680
+ if (opts.role === "task" && !usedAnyTool) {
681
+ return {
682
+ text: finalText,
683
+ error: "the model replied without calling any tools, so nothing was done",
684
+ failure: {
685
+ kind: "no-work",
686
+ detail: "model returned prose with no tool calls",
687
+ model: opts.model,
688
+ verbose: finalText,
689
+ },
690
+ usage: totalUsage,
691
+ playtestFrames: playtestFrameList(playtestFrames),
692
+ };
693
+ }
656
694
  return {
657
695
  text: finalText,
658
696
  usage: totalUsage,
659
697
  playtestFrames: playtestFrameList(playtestFrames),
660
698
  };
661
699
  }
700
+ usedAnyTool = true;
662
701
  messages.push({
663
702
  role: "assistant",
664
703
  content: streamResult.message.content || null,
@@ -1,3 +1,4 @@
1
+ import { type AgentFailure } from "../agent-failures.js";
1
2
  import type { NativeUsage } from "./types.js";
2
3
  export interface ORToolCall {
3
4
  id: string;
@@ -27,13 +28,17 @@ export interface ORAssistantMessage {
27
28
  content: string;
28
29
  tool_calls?: ORToolCall[];
29
30
  }
30
- export type ORReasoningEffort = "low" | "medium";
31
+ export type ORReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
32
+ export type ORRoutingMode = "balanced" | "nitro" | "exacto" | "floor";
33
+ export declare function applyRoutingMode(model: string, routing?: ORRoutingMode): string;
31
34
  export interface StreamChatOpts {
32
35
  apiKey: string;
33
36
  model: string;
34
37
  messages: ORMessage[];
35
38
  tools?: unknown[];
36
39
  reasoningEffort?: ORReasoningEffort;
40
+ routing?: ORRoutingMode;
41
+ providerTier?: string;
37
42
  maxTokens?: number;
38
43
  signal?: AbortSignal;
39
44
  onDelta?: (delta: string) => void;
@@ -51,5 +56,11 @@ export interface StreamChatResult {
51
56
  reasoningTokens?: number;
52
57
  crashed: boolean;
53
58
  error?: string;
59
+ failure?: AgentFailure;
60
+ }
61
+ export declare class OpenrouterHttpError extends Error {
62
+ readonly status: number;
63
+ readonly body: string;
64
+ constructor(status: number, body: string);
54
65
  }
55
66
  export declare function streamChatCompletion(opts: StreamChatOpts): Promise<StreamChatResult>;
@@ -13,6 +13,7 @@
13
13
  // the model already said, which the caller (native/loop.ts) is better placed
14
14
  // to decide (or simply not do, matching today's crash-then-relaunch policy at
15
15
  // the task-attempt level).
16
+ import { failureForStatus } from "../agent-failures.js";
16
17
  // Overridable via CASTLE_OPENROUTER_URL so the QA harness (see
17
18
  // scripts/tests/agent-qa/native/fake-openrouter.mjs) can point this client at
18
19
  // a local fake server instead of the real API -- read per-call (not hoisted
@@ -24,6 +25,20 @@ function openrouterUrl() {
24
25
  const DEFAULT_MAX_RETRIES = 2; // -> 3 total connect attempts
25
26
  const RETRY_BASE_MS = 500;
26
27
  const RETRY_MAX_MS = 4_000;
28
+ const ROUTING_SUFFIX = {
29
+ nitro: ":nitro",
30
+ exacto: ":exacto",
31
+ floor: ":floor",
32
+ };
33
+ // Append the routing-mode suffix to a model slug, first stripping any suffix
34
+ // we might have added on a previous turn (or that the user typed into the
35
+ // free-form slug field) so switching modes doesn't stack `:nitro:floor`.
36
+ export function applyRoutingMode(model, routing) {
37
+ const base = model.replace(/:(nitro|exacto|floor)$/, "");
38
+ if (!routing || routing === "balanced")
39
+ return base;
40
+ return base + ROUTING_SUFFIX[routing];
41
+ }
27
42
  function sleep(ms) {
28
43
  return new Promise((resolve) => setTimeout(resolve, ms));
29
44
  }
@@ -34,6 +49,22 @@ function backoffMs(attempt) {
34
49
  function isRetryableStatus(status) {
35
50
  return status === 429 || status >= 500;
36
51
  }
52
+ // A non-retryable HTTP error from OpenRouter, carrying the status as a NUMBER
53
+ // rather than only baked into a message. This path knows the status
54
+ // first-hand, so classification upstream is a lookup instead of a regex over
55
+ // our own error prose (the claude-CLI path has no such luxury -- see
56
+ // classifyProviderError). `message` keeps the old "HTTP <status>: <body>"
57
+ // wording so logs and any string-matching callers read the same as before.
58
+ export class OpenrouterHttpError extends Error {
59
+ status;
60
+ body;
61
+ constructor(status, body) {
62
+ super(`HTTP ${status}${body ? `: ${body.slice(0, 300)}` : ""}`);
63
+ this.status = status;
64
+ this.body = body;
65
+ this.name = "OpenrouterHttpError";
66
+ }
67
+ }
37
68
  async function safeReadText(res) {
38
69
  try {
39
70
  return (await res.text()).trim();
@@ -79,7 +110,7 @@ async function connectWithRetry(init, opts) {
79
110
  continue;
80
111
  }
81
112
  const bodyText = await safeReadText(res);
82
- throw new Error(`HTTP ${res.status}${bodyText ? `: ${bodyText.slice(0, 300)}` : ""}`);
113
+ throw new OpenrouterHttpError(res.status, bodyText);
83
114
  }
84
115
  // Unreachable (the loop above always returns or throws), but keeps the
85
116
  // return type honest for TS.
@@ -188,7 +219,8 @@ function finalizeToolCalls(pending) {
188
219
  }
189
220
  export async function streamChatCompletion(opts) {
190
221
  const body = {
191
- model: opts.model,
222
+ // Routing mode rides the slug as a suffix (see applyRoutingMode).
223
+ model: applyRoutingMode(opts.model, opts.routing),
192
224
  messages: opts.messages,
193
225
  stream: true,
194
226
  // Deprecated on OpenRouter's side (usage is always included now) but
@@ -199,6 +231,10 @@ export async function streamChatCompletion(opts) {
199
231
  // See ORReasoningEffort / StreamChatOpts.reasoningEffort above for the
200
232
  // doc reference and the graceful-degradation guarantee this relies on.
201
233
  ...(opts.reasoningEffort ? { reasoning: { effort: opts.reasoningEffort } } : {}),
234
+ // Pin a provider tier when requested (see StreamChatOpts.providerTier).
235
+ ...(opts.providerTier
236
+ ? { provider: { order: [opts.providerTier], allow_fallbacks: true } }
237
+ : {}),
202
238
  // See StreamChatOpts.maxTokens above -- bounds one completion call's
203
239
  // total output (content + tool-call arguments + reasoning).
204
240
  ...(opts.maxTokens !== undefined ? { max_tokens: opts.maxTokens } : {}),
@@ -223,6 +259,13 @@ export async function streamChatCompletion(opts) {
223
259
  message: null,
224
260
  crashed: false,
225
261
  error: `could not run openrouter: ${err instanceof Error ? err.message : String(err)}`,
262
+ // Only an HTTP error carries a status worth classifying. A network-level
263
+ // failure (DNS, refused, reset) stays unlabelled so it keeps landing in
264
+ // the pre-existing "spawn" bucket via the "could not run" prefix --
265
+ // that's already the right read: the request never got going.
266
+ failure: err instanceof OpenrouterHttpError
267
+ ? failureForStatus(err.status, err.body)
268
+ : undefined,
226
269
  };
227
270
  }
228
271
  if (!res.body) {
@@ -1,4 +1,6 @@
1
+ import type { AgentFailure } from "../agent-failures.js";
1
2
  import type { PlaytestExecutor } from "./playtest.js";
3
+ import type { ORReasoningEffort, ORRoutingMode } from "./openrouter.js";
2
4
  export type NativeRole = "router" | "task";
3
5
  export interface NativePlaytestOpts {
4
6
  executor: PlaytestExecutor;
@@ -16,6 +18,9 @@ export interface NativeRunOpts {
16
18
  role: NativeRole;
17
19
  model: string;
18
20
  apiKey: string;
21
+ reasoningEffort?: ORReasoningEffort;
22
+ routing?: ORRoutingMode;
23
+ providerTier?: string;
19
24
  prompt: string;
20
25
  systemReminder?: string;
21
26
  attachments?: string[];
@@ -33,6 +38,7 @@ export interface NativeRunOpts {
33
38
  export interface NativeRunResult {
34
39
  text: string;
35
40
  error?: string;
41
+ failure?: AgentFailure;
36
42
  usage?: NativeUsage;
37
43
  playtestFrames?: string[];
38
44
  crashed?: boolean;
@@ -1,41 +1,3 @@
1
1
  // Shapes for the native (in-process, OpenRouter-backed) agent backend --
2
2
  // see the plan at native_openrouter_agent_loop_fd7d9b3e.plan.md, "Path B".
3
- //
4
- // These MIRROR the non-exported CliRunOpts / CliRunResult / CliUsage
5
- // interfaces in cli/src/agent.ts (see runAgentCli there), which normalize
6
- // the headless `claude` / `cursor-agent` CLI backends into onDelta/onActivity/
7
- // onThinking hooks plus a { finalText, error, usage, crashed } result. This
8
- // file is a SEPARATE set of types for now (new-files-only
9
- // constraint -- agent.ts is being edited concurrently by a sibling change),
10
- // not a re-export, so field names differ in a few places. When the native
11
- // backend is wired into agent.ts, expect one of these two outcomes:
12
- //
13
- // 1. CliRunOpts/CliRunResult are widened to a shape both backends share
14
- // (command/args/parser and children become backend-specific extras), or
15
- // 2. the call site maps between the two shapes directly:
16
- // - NativeRunResult.text -> CliRunResult.finalText
17
- // - NativeRunResult.error/usage/crashed -> same names
18
- // - CliRunResult.ok <- derived as
19
- // `!result.error && !result.crashed` (no native equivalent of
20
- // a process exit code -- "ok" always follows from the other
21
- // two fields)
22
- // - CliRunOpts.children (Set<ChildProcess>) has no native
23
- // equivalent -- runAgentNative takes `signal` (AbortSignal)
24
- // instead, so a caller cancels a run the same way it would abort
25
- // any other fetch-based operation. The wiring step would create one
26
- // AbortController per run and adapt it to whatever cancellation
27
- // registry agent.ts uses for CLI children.
28
- // - CliRunOpts.command/args/parser have no native equivalent --
29
- // replaced by `model` (the free-form OpenRouter model id) and
30
- // `apiKey`.
31
- // - CliRunOpts.logPath maps to NativeRunOpts.logPath: same file
32
- // locations (tasks/<id>/log.jsonl, .castle/agent/router-log.jsonl),
33
- // same append-JSONL discipline, but the native lines are STRUCTURED
34
- // run events (init/assistant/tool_result/eviction/retry/result --
35
- // see createRunLogger in loop.ts) rather than raw CLI stream-json.
36
- //
37
- // Turns stay stateless exactly as they are today: the caller rebuilds one big
38
- // prompt string per turn (buildRouterPrompt/buildTaskPrompt) and passes it as
39
- // `prompt`; runAgentNative holds the resulting message array only for the
40
- // lifetime of that one call.
41
3
  export {};
@@ -0,0 +1,28 @@
1
+ export interface CatalogEntry {
2
+ id: string;
3
+ supportedParameters: string[];
4
+ reasoning: unknown;
5
+ }
6
+ export type ModelCheck = {
7
+ status: "ok";
8
+ } | {
9
+ status: "unknown-model";
10
+ suggestions: string[];
11
+ } | {
12
+ status: "no-tools";
13
+ } | {
14
+ status: "unavailable";
15
+ };
16
+ export declare function primeOpenrouterCatalog(): void;
17
+ export type KeyCheck = {
18
+ status: "ok";
19
+ } | {
20
+ status: "bad-key";
21
+ } | {
22
+ status: "no-credits";
23
+ } | {
24
+ status: "unavailable";
25
+ };
26
+ export declare function checkOpenrouterKey(apiKey: string): Promise<KeyCheck>;
27
+ export declare function checkOpenrouterModel(slug: string): Promise<ModelCheck>;
28
+ export declare function openrouterCatalogEntry(slug: string): Promise<CatalogEntry | null>;