castle-web-cli 0.4.175 → 0.4.177
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-failures.d.ts +2 -0
- package/dist/agent-failures.js +34 -1
- package/dist/agent.js +23 -13
- package/dist/metering.d.ts +11 -0
- package/dist/metering.js +33 -0
- package/dist/serve.js +3 -1
- package/dist/shell/assets/{index-istRh9xQ.js → index-BWOEraUy.js} +33 -33
- package/dist/shell/index.html +1 -1
- package/package.json +7 -3
package/dist/agent-failures.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface AgentFailure {
|
|
|
9
9
|
suggestion?: string;
|
|
10
10
|
resetAtMs?: number;
|
|
11
11
|
castleCreditsExhausted?: boolean;
|
|
12
|
+
castleCreditsInsufficient?: boolean;
|
|
13
|
+
castleSpendLimit?: boolean;
|
|
12
14
|
}
|
|
13
15
|
export declare function failureForStatus(status: number, body: string, model?: string): AgentFailure | undefined;
|
|
14
16
|
export declare function classifyProviderError(text: string | undefined, model?: string): AgentFailure | undefined;
|
package/dist/agent-failures.js
CHANGED
|
@@ -46,8 +46,35 @@ export function failureForStatus(status, body, model) {
|
|
|
46
46
|
return config("bad-key");
|
|
47
47
|
if (status === 402)
|
|
48
48
|
return config("no-credits");
|
|
49
|
-
if (status === 403)
|
|
49
|
+
if (status === 403) {
|
|
50
|
+
// Castle's proxy deliberately uses provider-shaped 403s for its own spend
|
|
51
|
+
// and model gates. A request-size reservation can refuse while the account
|
|
52
|
+
// still has a small positive balance, so pre-flight cannot catch this case.
|
|
53
|
+
// Keep the matches tied to the proxy's Castle-specific copy: a provider's
|
|
54
|
+
// ordinary permission/content 403 must continue to read as a refusal.
|
|
55
|
+
if (/Castle AI request[^.]*balance can(?:not|'t) cover|add credits at castle\.xyz\/credits/i.test(body)) {
|
|
56
|
+
return {
|
|
57
|
+
kind: "limit",
|
|
58
|
+
detail,
|
|
59
|
+
model,
|
|
60
|
+
verbose: `HTTP ${status}: ${body}`,
|
|
61
|
+
castleCreditsInsufficient: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (/model\s+.+\s+(?:isn['’]t|is not) available on this Castle account/i.test(body)) {
|
|
65
|
+
return config("model-not-allowed");
|
|
66
|
+
}
|
|
67
|
+
if (/Castle AI spending limit reached/i.test(body)) {
|
|
68
|
+
return {
|
|
69
|
+
kind: "limit",
|
|
70
|
+
detail,
|
|
71
|
+
model,
|
|
72
|
+
verbose: `HTTP ${status}: ${body}`,
|
|
73
|
+
castleSpendLimit: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
50
76
|
return config("flagged");
|
|
77
|
+
}
|
|
51
78
|
if (status === 429 || status >= 500) {
|
|
52
79
|
return { kind: "transient", detail, model, verbose: `HTTP ${status}: ${body}` };
|
|
53
80
|
}
|
|
@@ -178,9 +205,15 @@ export function failureCopy(opts) {
|
|
|
178
205
|
case "config":
|
|
179
206
|
return `${configCopy(opts.failure)}${tasksNote}`;
|
|
180
207
|
case "limit":
|
|
208
|
+
if (opts.failure.castleCreditsInsufficient) {
|
|
209
|
+
return `Not enough Castle AI credits for this request — get more at castle.xyz/credits${tasksNote}`;
|
|
210
|
+
}
|
|
181
211
|
if (opts.failure.castleCreditsExhausted) {
|
|
182
212
|
return `Out of Castle AI credits — get more at castle.xyz/credits${tasksNote}`;
|
|
183
213
|
}
|
|
214
|
+
if (opts.failure.castleSpendLimit) {
|
|
215
|
+
return `Castle AI spending limit reached — check your balance at castle.xyz/credits${tasksNote}`;
|
|
216
|
+
}
|
|
184
217
|
return `Daily Castle AI limit reached${resetsClause(opts.failure.resetAtMs)}. Runs on your own API key or login aren't limited.${tasksNote}`;
|
|
185
218
|
case "transient":
|
|
186
219
|
return `OpenRouter is busy right now and I couldn't get through. Send that again in a moment.${tasksNote}`;
|
package/dist/agent.js
CHANGED
|
@@ -31,7 +31,7 @@ import { applyPlanOps, buildRouterPromptParts, buildTaskPrompt, parsePlanOps, pl
|
|
|
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';
|
|
34
|
-
import { castleCreditsExhausted, fetchAiCredits, fetchBudget, meteringHeaders, newAgentSessionId, reportCursorRun, spendableMicros, withCustomHeaders, } from './metering.js';
|
|
34
|
+
import { castleCreditsExhausted, createRefreshQueue, fetchAiCredits, fetchBudget, meteringHeaders, newAgentSessionId, reportCursorRun, spendableMicros, withCustomHeaders, } from './metering.js';
|
|
35
35
|
import { anthropicKeyHelperCommand, claudeHasSavedLogin, cursorAuthPath, cursorHasUserLogin, resolveAnthropicAuth, userKey, ANTHROPIC_CREDENTIAL_ENV, ANTHROPIC_PROXY_ENV, CASTLE_USER_KEYS_PATH, } from './byo-auth.js';
|
|
36
36
|
import { accountsSnapshot, loginProviderFor, watchCredentials, writeCredential, } from './byo-accounts.js';
|
|
37
37
|
import { cancelLogin, logout, startLogin, submitLoginCode } from './byo-login.js';
|
|
@@ -2101,19 +2101,24 @@ function createUsageFeed(opts) {
|
|
|
2101
2101
|
let latest = null;
|
|
2102
2102
|
let credits = null;
|
|
2103
2103
|
let budgetPolls = 0;
|
|
2104
|
-
async function
|
|
2104
|
+
async function readUsage(refreshCredits = false) {
|
|
2105
2105
|
if (!opts.castlePaid()) {
|
|
2106
2106
|
credits = null;
|
|
2107
|
-
|
|
2108
|
-
return;
|
|
2107
|
+
return null;
|
|
2109
2108
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2109
|
+
// Budget is the authoritative spendable amount and its read can force a
|
|
2110
|
+
// stale proxy gate to sync after a purchase. Read credits second so the
|
|
2111
|
+
// descriptive fields (plan, rate, reload state) describe that same or a
|
|
2112
|
+
// newer account snapshot instead of racing ahead of the gate refresh.
|
|
2113
|
+
const budget = await fetchBudget();
|
|
2114
|
+
if (refreshCredits) {
|
|
2115
|
+
const nextCredits = await fetchAiCredits();
|
|
2116
|
+
// Once the richer field has loaded, a transient GraphQL failure should
|
|
2117
|
+
// not erase it and fall the UI back to the legacy daily presentation.
|
|
2118
|
+
if (nextCredits)
|
|
2119
|
+
credits = nextCredits;
|
|
2120
|
+
}
|
|
2121
|
+
return usageFrame(budget, credits);
|
|
2117
2122
|
}
|
|
2118
2123
|
function publishUsage(next) {
|
|
2119
2124
|
if (JSON.stringify(next ?? null) === JSON.stringify(latest ?? null))
|
|
@@ -2121,13 +2126,18 @@ function createUsageFeed(opts) {
|
|
|
2121
2126
|
latest = next;
|
|
2122
2127
|
opts.broadcast({ type: 'usage', usage: next });
|
|
2123
2128
|
}
|
|
2124
|
-
|
|
2129
|
+
// Focus, host relay, popover-open and run-finished can all arrive together.
|
|
2130
|
+
const refreshQueue = createRefreshQueue({ read: readUsage, publish: publishUsage });
|
|
2131
|
+
const requestRefresh = (withCredits) => {
|
|
2132
|
+
void refreshQueue.request(withCredits);
|
|
2133
|
+
};
|
|
2134
|
+
const refresh = () => requestRefresh(true);
|
|
2125
2135
|
const stopRunWatch = onAgentRunFinished(refresh);
|
|
2126
2136
|
const timer = setInterval(() => {
|
|
2127
2137
|
if (!opts.hasClients())
|
|
2128
2138
|
return;
|
|
2129
2139
|
budgetPolls += 1;
|
|
2130
|
-
|
|
2140
|
+
requestRefresh(budgetPolls % 5 === 0);
|
|
2131
2141
|
}, USAGE_POLL_MS);
|
|
2132
2142
|
timer.unref?.();
|
|
2133
2143
|
refresh();
|
package/dist/metering.d.ts
CHANGED
|
@@ -66,6 +66,17 @@ export interface AiCredits {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
export declare function spendableMicros(budget: CastleBudget): number | null;
|
|
69
|
+
/**
|
|
70
|
+
* Coalesce asynchronous refresh triggers without letting an older read finish
|
|
71
|
+
* after and overwrite a newer one. `withFullState` is sticky across queued
|
|
72
|
+
* calls: a cheap poll cannot downgrade a pending user-requested full refresh.
|
|
73
|
+
*/
|
|
74
|
+
export declare function createRefreshQueue<T>(opts: {
|
|
75
|
+
read: (withFullState: boolean) => Promise<T>;
|
|
76
|
+
publish: (value: T) => void;
|
|
77
|
+
}): {
|
|
78
|
+
request: (withFullState: boolean) => Promise<void>;
|
|
79
|
+
};
|
|
69
80
|
export declare function castleCreditsExhausted(budget: CastleBudget, credits: AiCredits | null): boolean;
|
|
70
81
|
/** Rich credit state, or null when ghost/proxy cannot provide the new field. */
|
|
71
82
|
export declare function fetchAiCredits(): Promise<AiCredits | null>;
|
package/dist/metering.js
CHANGED
|
@@ -120,6 +120,39 @@ export function spendableMicros(budget) {
|
|
|
120
120
|
return null;
|
|
121
121
|
return Math.max(0, budget.limitMicros - budget.usedMicros);
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Coalesce asynchronous refresh triggers without letting an older read finish
|
|
125
|
+
* after and overwrite a newer one. `withFullState` is sticky across queued
|
|
126
|
+
* calls: a cheap poll cannot downgrade a pending user-requested full refresh.
|
|
127
|
+
*/
|
|
128
|
+
export function createRefreshQueue(opts) {
|
|
129
|
+
let queued = false;
|
|
130
|
+
let queuedFullState = false;
|
|
131
|
+
let active = null;
|
|
132
|
+
const request = (withFullState) => {
|
|
133
|
+
queued = true;
|
|
134
|
+
queuedFullState ||= withFullState;
|
|
135
|
+
if (active)
|
|
136
|
+
return active;
|
|
137
|
+
active = (async () => {
|
|
138
|
+
while (queued) {
|
|
139
|
+
const fullState = queuedFullState;
|
|
140
|
+
queued = false;
|
|
141
|
+
queuedFullState = false;
|
|
142
|
+
const value = await opts.read(fullState);
|
|
143
|
+
// If another trigger arrived while this read was in flight, its result
|
|
144
|
+
// is already the one the caller wants. Skip the older intermediate
|
|
145
|
+
// frame instead of flashing it before the trailing refresh completes.
|
|
146
|
+
if (!queued)
|
|
147
|
+
opts.publish(value);
|
|
148
|
+
}
|
|
149
|
+
})().finally(() => {
|
|
150
|
+
active = null;
|
|
151
|
+
});
|
|
152
|
+
return active;
|
|
153
|
+
};
|
|
154
|
+
return { request };
|
|
155
|
+
}
|
|
123
156
|
const EXHAUSTED_BALANCE_CREDITS = 50;
|
|
124
157
|
export function castleCreditsExhausted(budget, credits) {
|
|
125
158
|
if (!budget.blocked || credits?.plan !== "credits")
|
package/dist/serve.js
CHANGED
|
@@ -1021,11 +1021,13 @@ async function serveDetached(projectDir, options) {
|
|
|
1021
1021
|
// this serve and it dies with the session.
|
|
1022
1022
|
// `.unref()` is what lets the calling node process exit either way.
|
|
1023
1023
|
const fullDetach = process.env.CASTLE_WEB_CLI_DETACH === '1';
|
|
1024
|
+
const childEnv = { ...process.env };
|
|
1025
|
+
delete childEnv.CASTLE_WEB_CLI_DETACH;
|
|
1024
1026
|
const child = spawn(process.execPath, [entry, ...args], {
|
|
1025
1027
|
cwd: projectDir,
|
|
1026
1028
|
detached: fullDetach,
|
|
1027
1029
|
stdio: ['ignore', logFd, logFd],
|
|
1028
|
-
env:
|
|
1030
|
+
env: childEnv,
|
|
1029
1031
|
});
|
|
1030
1032
|
child.unref();
|
|
1031
1033
|
fs.closeSync(logFd);
|