castle-web-cli 0.4.103 → 0.4.105
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
CHANGED
|
@@ -11,6 +11,12 @@ export interface AgentFailure {
|
|
|
11
11
|
}
|
|
12
12
|
export declare function failureForStatus(status: number, body: string, model?: string): AgentFailure | undefined;
|
|
13
13
|
export declare function classifyProviderError(text: string | undefined, model?: string): AgentFailure | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Record the reader's IANA zone. Junk is ignored rather than stored: an invalid
|
|
16
|
+
* zone makes toLocaleTimeString throw, and the one place that formatting runs is
|
|
17
|
+
* the error path, which must not fail.
|
|
18
|
+
*/
|
|
19
|
+
export declare function setReaderTimeZone(zone: string): void;
|
|
14
20
|
export declare function failureCopy(opts: {
|
|
15
21
|
failure: AgentFailure;
|
|
16
22
|
spawnedTasks: boolean;
|
package/dist/agent-failures.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// what's wrong and points at settings / Castle.
|
|
11
11
|
// limit - Castle's daily AI allowance for this user is spent. Nothing to
|
|
12
12
|
// fix in the session and nothing to retry until the reset, so the
|
|
13
|
-
// copy gives the time and the one way
|
|
13
|
+
// copy gives the time (see setReaderTimeZone) and the one way
|
|
14
|
+
// around it (own credential).
|
|
14
15
|
// transient - the provider was busy or broke. NOT auto-retried either (see
|
|
15
16
|
// below), but the copy invites the user to send again.
|
|
16
17
|
// no-work - the model answered instead of working. Config is fine; the
|
|
@@ -98,6 +99,41 @@ export function classifyProviderError(text, model) {
|
|
|
98
99
|
function quoted(model) {
|
|
99
100
|
return model ? `"${model}"` : "the model this session is set to";
|
|
100
101
|
}
|
|
102
|
+
// The zone the person reading the copy is in, as the shell reported it on
|
|
103
|
+
// connect. Module state rather than a parameter because it is a property of the
|
|
104
|
+
// one reader this serve has, not of any particular failure, and every call site
|
|
105
|
+
// would otherwise thread it through a ctx that has no other reason to know.
|
|
106
|
+
let readerTimeZone;
|
|
107
|
+
/**
|
|
108
|
+
* Record the reader's IANA zone. Junk is ignored rather than stored: an invalid
|
|
109
|
+
* zone makes toLocaleTimeString throw, and the one place that formatting runs is
|
|
110
|
+
* the error path, which must not fail.
|
|
111
|
+
*/
|
|
112
|
+
export function setReaderTimeZone(zone) {
|
|
113
|
+
try {
|
|
114
|
+
new Intl.DateTimeFormat(undefined, { timeZone: zone });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
readerTimeZone = zone;
|
|
120
|
+
}
|
|
121
|
+
// The reset is a Pacific midnight and this process runs on the sandbox's clock
|
|
122
|
+
// (UTC), so a server-local time is wrong for every reader. Format in the zone
|
|
123
|
+
// the shell reported; with none reported, name the zone we did use rather than
|
|
124
|
+
// print a bare time that reads as local and isn't.
|
|
125
|
+
function resetsClause(atMs) {
|
|
126
|
+
if (!atMs)
|
|
127
|
+
return "";
|
|
128
|
+
const time = new Date(atMs).toLocaleTimeString(undefined, {
|
|
129
|
+
hour: "numeric",
|
|
130
|
+
minute: "2-digit",
|
|
131
|
+
...(readerTimeZone
|
|
132
|
+
? { timeZone: readerTimeZone }
|
|
133
|
+
: { timeZoneName: "short" }),
|
|
134
|
+
});
|
|
135
|
+
return ` -- resets ${time}`;
|
|
136
|
+
}
|
|
101
137
|
// Plain-language copy for a failed run. Deliberately ONE table rather than a
|
|
102
138
|
// router copy and a task copy: the sentences would be near-identical, and the
|
|
103
139
|
// duplicate-detection gate (jscpd, threshold 0) is right to reject that.
|
|
@@ -140,12 +176,8 @@ export function failureCopy(opts) {
|
|
|
140
176
|
switch (opts.failure.kind) {
|
|
141
177
|
case "config":
|
|
142
178
|
return `${configCopy(opts.failure)}${tasksNote}`;
|
|
143
|
-
case "limit":
|
|
144
|
-
|
|
145
|
-
? ` -- resets ${new Date(opts.failure.resetAtMs).toLocaleString()}`
|
|
146
|
-
: "";
|
|
147
|
-
return `Daily Castle AI limit reached${resets}. Runs on your own API key or login aren't limited.${tasksNote}`;
|
|
148
|
-
}
|
|
179
|
+
case "limit":
|
|
180
|
+
return `Daily Castle AI limit reached${resetsClause(opts.failure.resetAtMs)}. Runs on your own API key or login aren't limited.${tasksNote}`;
|
|
149
181
|
case "transient":
|
|
150
182
|
return `OpenRouter is busy right now and I couldn't get through. Send that again in a moment.${tasksNote}`;
|
|
151
183
|
case "no-work":
|
package/dist/agent.js
CHANGED
|
@@ -23,7 +23,7 @@ import { WebSocketServer } from "ws";
|
|
|
23
23
|
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
|
-
import { classifyProviderError, failureCopy, } from "./agent-failures.js";
|
|
26
|
+
import { classifyProviderError, failureCopy, setReaderTimeZone, } from "./agent-failures.js";
|
|
27
27
|
import { fetchBudget, meteringHeaders, newAgentSessionId, reportCursorRun, withCustomHeaders, } from "./metering.js";
|
|
28
28
|
import { anthropicKeyHelperCommand, claudeHasSavedLogin, resolveAnthropicAuth, userKey, ANTHROPIC_CREDENTIAL_ENV, ANTHROPIC_PROXY_ENV, CASTLE_USER_KEYS_PATH, } from "./byo-auth.js";
|
|
29
29
|
import { runAgentNative } from "./native/loop.js";
|
|
@@ -41,12 +41,14 @@ export const AGENT_MODEL_CAPS_PREFIX = "/__castle/agent/model-caps";
|
|
|
41
41
|
const DEFAULT_SETTINGS = {
|
|
42
42
|
router: "claude",
|
|
43
43
|
tasks: "claude",
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
44
|
+
// Both roles run the claude CLI routed through OpenRouter at the slug below.
|
|
45
|
+
// Note this takes the Anthropic credential out of play entirely: a role on
|
|
46
|
+
// "openrouter" resolves OpenRouter auth, so a user's `claude /login` or
|
|
47
|
+
// ANTHROPIC_API_KEY no longer applies (see buildAgentInvocation).
|
|
48
|
+
routerClaudeModel: "openrouter",
|
|
49
|
+
tasksClaudeModel: "openrouter",
|
|
48
50
|
// Free-form -- change to any OpenRouter slug.
|
|
49
|
-
routerOpenrouterModel: "openai/gpt-5.6-
|
|
51
|
+
routerOpenrouterModel: "openai/gpt-5.6-terra",
|
|
50
52
|
tasksOpenrouterModel: "openai/gpt-5.6-terra",
|
|
51
53
|
// Both roles think at "medium": the operator stays snappy (the user waits
|
|
52
54
|
// on every operator turn), and task agents' multi-turn tool loops don't pay
|
|
@@ -60,10 +62,10 @@ const DEFAULT_SETTINGS = {
|
|
|
60
62
|
// which matters for tool-calling fidelity over long loops).
|
|
61
63
|
routerRouting: "nitro",
|
|
62
64
|
tasksRouting: "exacto",
|
|
63
|
-
// Operator pins OpenAI's priority (low-latency SLA) tier
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
65
|
+
// Operator pins OpenAI's priority (low-latency SLA) tier; harmless with a
|
|
66
|
+
// slug that lacks it since the pin falls back when the tag doesn't exist
|
|
67
|
+
// (allow_fallbacks). Tasks stay on auto: high-volume background turns
|
|
68
|
+
// should ride the cheapest available capacity.
|
|
67
69
|
routerProviderTier: "openai/priority",
|
|
68
70
|
tasksProviderTier: "",
|
|
69
71
|
};
|
|
@@ -72,16 +74,14 @@ function normalizeBackend(value) {
|
|
|
72
74
|
? value
|
|
73
75
|
: null;
|
|
74
76
|
}
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return normalizeBackend(value);
|
|
84
|
-
}
|
|
77
|
+
// Bump to force every deck onto the current DEFAULT_SETTINGS once, discarding
|
|
78
|
+
// what users had chosen: a stored file below this epoch has ALL of its setting
|
|
79
|
+
// fields dropped on load (see loadAgentSettings). Bump ONLY when a default
|
|
80
|
+
// change is meant to override existing choices -- an ordinary one needs no
|
|
81
|
+
// bump, since sparse storage already reaches anyone who never set that field.
|
|
82
|
+
// The wipe is also what retires older file shapes, so a migration for one is
|
|
83
|
+
// only worth writing if it must survive the epoch that introduces it.
|
|
84
|
+
const SETTINGS_EPOCH = 1;
|
|
85
85
|
function normalizeClaudeModel(value) {
|
|
86
86
|
return value === "sonnet" ||
|
|
87
87
|
value === "opus" ||
|
|
@@ -141,6 +141,60 @@ function normalizeProviderTier(value) {
|
|
|
141
141
|
return null;
|
|
142
142
|
return trimmed;
|
|
143
143
|
}
|
|
144
|
+
// Every setting's validator, keyed the same as AgentSettings, so load and save
|
|
145
|
+
// iterate one list rather than repeating the field names per direction.
|
|
146
|
+
const SETTING_NORMALIZERS = {
|
|
147
|
+
router: normalizeBackend,
|
|
148
|
+
tasks: normalizeBackend,
|
|
149
|
+
routerClaudeModel: normalizeClaudeModel,
|
|
150
|
+
tasksClaudeModel: normalizeClaudeModel,
|
|
151
|
+
routerOpenrouterModel: normalizeOpenrouterModel,
|
|
152
|
+
tasksOpenrouterModel: normalizeOpenrouterModel,
|
|
153
|
+
routerReasoningEffort: normalizeReasoningEffort,
|
|
154
|
+
tasksReasoningEffort: normalizeReasoningEffort,
|
|
155
|
+
routerRouting: normalizeRoutingMode,
|
|
156
|
+
tasksRouting: normalizeRoutingMode,
|
|
157
|
+
routerProviderTier: normalizeProviderTier,
|
|
158
|
+
tasksProviderTier: normalizeProviderTier,
|
|
159
|
+
};
|
|
160
|
+
const SETTING_KEYS = Object.keys(SETTING_NORMALIZERS);
|
|
161
|
+
// Sparse ON PURPOSE: only fields that DIFFER from the current defaults are
|
|
162
|
+
// stored, so "absent" means "follow the default" and a later default change
|
|
163
|
+
// reaches every deck that never set that field. Writing the full object
|
|
164
|
+
// instead (as this once did) froze all 12 settings the first time a user
|
|
165
|
+
// touched any one of them, pinning models nobody had chosen.
|
|
166
|
+
//
|
|
167
|
+
// The flip side, accepted: re-selecting the value that is currently the
|
|
168
|
+
// default stores nothing, so that deck moves when the default moves. Nothing
|
|
169
|
+
// is lost relative to the old behavior -- applyAgentSettings already
|
|
170
|
+
// early-returns on a no-op change, so that click never persisted anything.
|
|
171
|
+
function serializeAgentSettings(settings) {
|
|
172
|
+
const out = { settingsEpoch: SETTINGS_EPOCH };
|
|
173
|
+
for (const key of SETTING_KEYS) {
|
|
174
|
+
if (settings[key] !== DEFAULT_SETTINGS[key])
|
|
175
|
+
out[key] = settings[key];
|
|
176
|
+
}
|
|
177
|
+
return JSON.stringify(out, null, 2) + "\n";
|
|
178
|
+
}
|
|
179
|
+
function loadAgentSettings(settingsPath) {
|
|
180
|
+
const stored = readJsonFile(settingsPath);
|
|
181
|
+
const storedEpoch = typeof stored?.settingsEpoch === "number" ? stored.settingsEpoch : 0;
|
|
182
|
+
// A stale file is ignored, not rewritten: leaving it means the wipe is a
|
|
183
|
+
// pure read-side decision that repeats harmlessly until the user's next real
|
|
184
|
+
// change overwrites it in the sparse shape. A newer epoch (a downgraded CLI
|
|
185
|
+
// reading a file from a newer one) is honored rather than wiped.
|
|
186
|
+
if (!stored || storedEpoch < SETTINGS_EPOCH)
|
|
187
|
+
return { ...DEFAULT_SETTINGS };
|
|
188
|
+
const overrides = {};
|
|
189
|
+
for (const key of SETTING_KEYS) {
|
|
190
|
+
if (!(key in stored))
|
|
191
|
+
continue;
|
|
192
|
+
const value = SETTING_NORMALIZERS[key](stored[key]);
|
|
193
|
+
if (value !== null)
|
|
194
|
+
overrides[key] = value;
|
|
195
|
+
}
|
|
196
|
+
return { ...DEFAULT_SETTINGS, ...overrides };
|
|
197
|
+
}
|
|
144
198
|
// Base for OpenRouter's Anthropic-compatible endpoint (confirmed current, 2026: it
|
|
145
199
|
// accepts the standard Anthropic Messages API shape -- text/tool-use/extended-thinking
|
|
146
200
|
// -- for ANY OpenRouter model slug, not just Anthropic ones), which the claude CLI
|
|
@@ -2974,7 +3028,7 @@ function applyAgentSettings(incoming, ctx) {
|
|
|
2974
3028
|
}
|
|
2975
3029
|
if (changes.length === 0)
|
|
2976
3030
|
return;
|
|
2977
|
-
fs.writeFileSync(ctx.settingsPath,
|
|
3031
|
+
fs.writeFileSync(ctx.settingsPath, serializeAgentSettings(settings));
|
|
2978
3032
|
// The value is saved and broadcast IMMEDIATELY -- validation never gates a
|
|
2979
3033
|
// write. The verdict follows in a second frame once the catalog answers.
|
|
2980
3034
|
ctx.broadcast({ type: "settings", settings });
|
|
@@ -3448,59 +3502,7 @@ export function createAgentServer(opts) {
|
|
|
3448
3502
|
// Which CLI backs the router and the task agents -- independently
|
|
3449
3503
|
// switchable from the settings popover, persisted next to the chat state.
|
|
3450
3504
|
const settingsPath = path.join(agentDir, "settings.json");
|
|
3451
|
-
|
|
3452
|
-
// types no longer admit (backend "openrouter", single openrouterModel /
|
|
3453
|
-
// claudeModel) -- every field goes through a normalizer/migrator below.
|
|
3454
|
-
const storedSettings = readJsonFile(settingsPath);
|
|
3455
|
-
// Three legacy shapes migrate on load, chained oldest-first (rewritten in
|
|
3456
|
-
// the new shape on the next settings change):
|
|
3457
|
-
// - a single `openrouterModel` (pre per-role split) seeds BOTH per-role
|
|
3458
|
-
// slug fields;
|
|
3459
|
-
// - a single shared `claudeModel` (pre per-role split) seeds BOTH per-role
|
|
3460
|
-
// claude-model fields;
|
|
3461
|
-
// - backend value "openrouter" ON A ROLE (pre-smith: it meant that role
|
|
3462
|
-
// ran claude-via-OpenRouter) becomes backend "claude" + THAT role's
|
|
3463
|
-
// claude model "openrouter" -- exactly that behavior today, and now
|
|
3464
|
-
// per-role: one legacy openrouter role no longer drags the other role's
|
|
3465
|
-
// model along.
|
|
3466
|
-
const legacyOpenrouterModel = normalizeOpenrouterModel(storedSettings?.openrouterModel);
|
|
3467
|
-
const legacySharedClaudeModel = normalizeClaudeModel(storedSettings?.claudeModel);
|
|
3468
|
-
const roleClaudeModel = (role) => {
|
|
3469
|
-
// A stored per-role value wins; a legacy openrouter BACKEND on this role
|
|
3470
|
-
// forces "openrouter" (those two never coexist in one file -- per-role
|
|
3471
|
-
// fields postdate the openrouter backend's removal); else the legacy
|
|
3472
|
-
// shared claudeModel, else the default.
|
|
3473
|
-
return (normalizeClaudeModel(storedSettings?.[`${role}ClaudeModel`]) ??
|
|
3474
|
-
(storedSettings?.[role] === "openrouter"
|
|
3475
|
-
? "openrouter"
|
|
3476
|
-
: (legacySharedClaudeModel ?? DEFAULT_SETTINGS[`${role}ClaudeModel`])));
|
|
3477
|
-
};
|
|
3478
|
-
const settings = {
|
|
3479
|
-
router: migrateStoredBackend(storedSettings?.router) ?? DEFAULT_SETTINGS.router,
|
|
3480
|
-
tasks: migrateStoredBackend(storedSettings?.tasks) ?? DEFAULT_SETTINGS.tasks,
|
|
3481
|
-
routerClaudeModel: roleClaudeModel("router"),
|
|
3482
|
-
tasksClaudeModel: roleClaudeModel("tasks"),
|
|
3483
|
-
routerOpenrouterModel: normalizeOpenrouterModel(storedSettings?.routerOpenrouterModel) ??
|
|
3484
|
-
legacyOpenrouterModel ??
|
|
3485
|
-
DEFAULT_SETTINGS.routerOpenrouterModel,
|
|
3486
|
-
tasksOpenrouterModel: normalizeOpenrouterModel(storedSettings?.tasksOpenrouterModel) ??
|
|
3487
|
-
legacyOpenrouterModel ??
|
|
3488
|
-
DEFAULT_SETTINGS.tasksOpenrouterModel,
|
|
3489
|
-
routerReasoningEffort: normalizeReasoningEffort(storedSettings?.routerReasoningEffort) ??
|
|
3490
|
-
DEFAULT_SETTINGS.routerReasoningEffort,
|
|
3491
|
-
tasksReasoningEffort: normalizeReasoningEffort(storedSettings?.tasksReasoningEffort) ??
|
|
3492
|
-
DEFAULT_SETTINGS.tasksReasoningEffort,
|
|
3493
|
-
routerRouting: normalizeRoutingMode(storedSettings?.routerRouting) ??
|
|
3494
|
-
DEFAULT_SETTINGS.routerRouting,
|
|
3495
|
-
tasksRouting: normalizeRoutingMode(storedSettings?.tasksRouting) ??
|
|
3496
|
-
DEFAULT_SETTINGS.tasksRouting,
|
|
3497
|
-
// Provider tier: "" is valid (auto), so keep a normalized "" over the
|
|
3498
|
-
// default rather than treating it as absent.
|
|
3499
|
-
routerProviderTier: normalizeProviderTier(storedSettings?.routerProviderTier) ??
|
|
3500
|
-
DEFAULT_SETTINGS.routerProviderTier,
|
|
3501
|
-
tasksProviderTier: normalizeProviderTier(storedSettings?.tasksProviderTier) ??
|
|
3502
|
-
DEFAULT_SETTINGS.tasksProviderTier,
|
|
3503
|
-
};
|
|
3505
|
+
const settings = loadAgentSettings(settingsPath);
|
|
3504
3506
|
const usageFeed = createUsageFeed({
|
|
3505
3507
|
broadcast,
|
|
3506
3508
|
hasClients: () => clients.size > 0,
|
|
@@ -3623,6 +3625,9 @@ export function createAgentServer(opts) {
|
|
|
3623
3625
|
else if (msg.type === "set-settings") {
|
|
3624
3626
|
applySettings(msg);
|
|
3625
3627
|
}
|
|
3628
|
+
else if (msg.type === "client-timezone" && typeof msg.timeZone === "string") {
|
|
3629
|
+
setReaderTimeZone(msg.timeZone);
|
|
3630
|
+
}
|
|
3626
3631
|
});
|
|
3627
3632
|
socket.on("close", () => {
|
|
3628
3633
|
clients.delete(socket);
|