clementine-agent 1.18.14 → 1.18.16
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/README.md +10 -3
- package/dist/agent/assistant.js +30 -20
- package/dist/cli/dashboard.js +398 -5
- package/dist/cli/index.js +67 -15
- package/dist/config/config-doctor.js +20 -5
- package/dist/config/effective-config.js +2 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.js +78 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -324,14 +324,20 @@ clementine restart # apply changes
|
|
|
324
324
|
|
|
325
325
|
Your overrides live in `~/.clementine/.env` — **they survive every `npm update -g` / `clementine update`** because they're in your data home, not the package directory.
|
|
326
326
|
|
|
327
|
+
The dashboard exposes these spend controls in Settings -> Channels & Env ->
|
|
328
|
+
Spend Guards & Context Health, including direct dollar-cap editing, Default
|
|
329
|
+
Caps, Safe Recovery, and No Caps presets.
|
|
330
|
+
|
|
327
331
|
For spend/context tuning, `clementine budgets` gives a safer shortcut:
|
|
328
332
|
|
|
329
333
|
```bash
|
|
330
334
|
clementine budgets # show chat/cron/heartbeat caps and 1M context state
|
|
331
|
-
clementine budgets safe # lower background budgets and
|
|
332
|
-
clementine budgets 1m
|
|
335
|
+
clementine budgets safe # lower background budgets and force standard 200K context
|
|
336
|
+
clementine budgets 1m auto # allow included Opus 1M, keep Sonnet on 200K
|
|
337
|
+
clementine budgets 1m on # force 1M context for Extra Usage/API users
|
|
333
338
|
clementine budgets 1m off # disable 1M context for maximum compatibility
|
|
334
339
|
clementine budgets set chat 10 # raise one budget cap
|
|
340
|
+
clementine budgets set chat 0 # remove one cap
|
|
335
341
|
```
|
|
336
342
|
|
|
337
343
|
**Commonly tuned knobs:**
|
|
@@ -342,7 +348,8 @@ clementine budgets set chat 10 # raise one budget cap
|
|
|
342
348
|
| `BUDGET_CRON_T1_USD` | `0.75` | Max spend per tier-1 cron job |
|
|
343
349
|
| `BUDGET_CRON_T2_USD` | `1.50` | Max spend per tier-2 cron job |
|
|
344
350
|
| `BUDGET_HEARTBEAT_USD` | `0.25` | Max spend per heartbeat tick |
|
|
345
|
-
| `
|
|
351
|
+
| `CLEMENTINE_1M_CONTEXT_MODE` | `auto` | `auto` allows included Opus 1M on Max/Team/Enterprise while keeping Sonnet on 200K; `off` forces 200K; `on` forces 1M |
|
|
352
|
+
| `CLAUDE_CODE_DISABLE_1M_CONTEXT` | legacy | Backward-compatible Claude Code switch; `budgets safe` writes `1`, `budgets 1m auto` removes it |
|
|
346
353
|
| `DEFAULT_MODEL_TIER` | `sonnet` | Default model: `haiku` / `sonnet` / `opus` |
|
|
347
354
|
| `HEARTBEAT_INTERVAL_MINUTES` | `30` | How often the agent auto-checks in |
|
|
348
355
|
| `HEARTBEAT_ACTIVE_START` | `8` | First hour of the active window (0–23) |
|
package/dist/agent/assistant.js
CHANGED
|
@@ -13,7 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { query as rawQuery, listSubagents, getSubagentMessages, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import pino from 'pino';
|
|
16
|
-
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, HANDOFFS_DIR, BUDGET, TASK_BUDGET_TOKENS, IDENTITY_FILE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, envSnapshot, } from '../config.js';
|
|
16
|
+
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, HANDOFFS_DIR, BUDGET, TASK_BUDGET_TOKENS, IDENTITY_FILE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, claudeCodeDisableOneMillionForModel, currentOneMillionContextMode, normalizeClaudeModelForOneMillionContext, usesOneMillionContext, envSnapshot, } from '../config.js';
|
|
17
17
|
import { summarizeIntegrationStatus } from '../config/integrations-registry.js';
|
|
18
18
|
import { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock, } from '../integrations/tool-preferences.js';
|
|
19
19
|
import { loadClaudeIntegrations } from './mcp-bridge.js';
|
|
@@ -320,6 +320,8 @@ const MODEL_CONTEXT_WINDOWS = {
|
|
|
320
320
|
'opus': 200_000,
|
|
321
321
|
};
|
|
322
322
|
function getContextWindow(model) {
|
|
323
|
+
if (usesOneMillionContext(model))
|
|
324
|
+
return 1_000_000;
|
|
323
325
|
for (const [family, size] of Object.entries(MODEL_CONTEXT_WINDOWS)) {
|
|
324
326
|
if (model.includes(family))
|
|
325
327
|
return size;
|
|
@@ -338,10 +340,6 @@ function resultInputTokens(result) {
|
|
|
338
340
|
}
|
|
339
341
|
return total;
|
|
340
342
|
}
|
|
341
|
-
function oneMillionContextDisabled() {
|
|
342
|
-
const value = process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT;
|
|
343
|
-
return value === undefined || !/^(0|false|no)$/i.test(value);
|
|
344
|
-
}
|
|
345
343
|
export function looksLikeOneMillionContextError(value) {
|
|
346
344
|
const text = String(value ?? '');
|
|
347
345
|
return /extra usage.*1m context|1m context.*extra usage|context-1m/i.test(text);
|
|
@@ -537,12 +535,6 @@ function buildSafeEnv() {
|
|
|
537
535
|
sanitized.ANTHROPIC_API_KEY = apiKeyVal;
|
|
538
536
|
}
|
|
539
537
|
// When all are absent: HOME lets the subprocess find Keychain OAuth automatically.
|
|
540
|
-
// Preserve trusted Claude Code runtime flags set by config.ts. In
|
|
541
|
-
// particular, CLAUDE_CODE_DISABLE_1M_CONTEXT defaults on so background
|
|
542
|
-
// helper queries do not silently re-enable the 1M context beta.
|
|
543
|
-
if (process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT !== undefined) {
|
|
544
|
-
sanitized.CLAUDE_CODE_DISABLE_1M_CONTEXT = process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT;
|
|
545
|
-
}
|
|
546
538
|
// Step 3: Add trusted markers AFTER sanitization
|
|
547
539
|
sanitized.CLEMENTINE_HOME = BASE_DIR;
|
|
548
540
|
return sanitized;
|
|
@@ -2148,7 +2140,10 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2148
2140
|
&& toolsDisabledForCall
|
|
2149
2141
|
&& turnPolicy?.retrievalTier === 'none'
|
|
2150
2142
|
&& turnPolicy.effort === 'low';
|
|
2151
|
-
const
|
|
2143
|
+
const rawResolvedModel = resolveModel(requestedModel) ?? (lightweightModelEligible ? MODELS.haiku : MODEL);
|
|
2144
|
+
const resolvedModel = normalizeClaudeModelForOneMillionContext(rawResolvedModel);
|
|
2145
|
+
const oneMillionModeValue = currentOneMillionContextMode();
|
|
2146
|
+
const oneMillionDisableValue = claudeCodeDisableOneMillionForModel(resolvedModel);
|
|
2152
2147
|
const modelRouteReason = model
|
|
2153
2148
|
? 'explicit'
|
|
2154
2149
|
: profile?.model
|
|
@@ -2421,7 +2416,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2421
2416
|
systemPrompt: fullSystemPrompt,
|
|
2422
2417
|
model: resolvedModel,
|
|
2423
2418
|
...(fallback ? { fallbackModel: fallback } : {}),
|
|
2424
|
-
...(
|
|
2419
|
+
...(oneMillionDisableValue === '1' ? { betas: [] } : {}),
|
|
2425
2420
|
permissionMode: effectivePermissionMode,
|
|
2426
2421
|
allowDangerouslySkipPermissions: true,
|
|
2427
2422
|
...(sessionStore ? { sessionStore } : {}),
|
|
@@ -2459,6 +2454,10 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2459
2454
|
CLEMENTINE_TEAM_AGENT: profile?.slug ?? 'clementine',
|
|
2460
2455
|
CLEMENTINE_INTERACTION_SOURCE: sourceOverride ?? inferInteractionSource(sessionKey),
|
|
2461
2456
|
CLEMENTINE_TOOL_ALLOWLIST: clementineToolAllowlist,
|
|
2457
|
+
CLEMENTINE_1M_CONTEXT_MODE: oneMillionModeValue,
|
|
2458
|
+
...(oneMillionDisableValue !== undefined
|
|
2459
|
+
? { CLAUDE_CODE_DISABLE_1M_CONTEXT: oneMillionDisableValue }
|
|
2460
|
+
: {}),
|
|
2462
2461
|
},
|
|
2463
2462
|
},
|
|
2464
2463
|
...externalMcpServers,
|
|
@@ -2472,14 +2471,21 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2472
2471
|
// env only when the prompt/job mentions a connector-backed service.
|
|
2473
2472
|
// Per-MCP-server env isolation still happens inside each mcpServers
|
|
2474
2473
|
// entry; this only affects the Claude Code subprocess itself.
|
|
2475
|
-
|
|
2476
|
-
|
|
2474
|
+
env: shouldInheritClaudeEnv
|
|
2475
|
+
? {
|
|
2476
|
+
...process.env,
|
|
2477
|
+
CLEMENTINE_1M_CONTEXT_MODE: oneMillionModeValue,
|
|
2478
|
+
...(oneMillionDisableValue !== undefined
|
|
2479
|
+
? { CLAUDE_CODE_DISABLE_1M_CONTEXT: oneMillionDisableValue }
|
|
2480
|
+
: {}),
|
|
2481
|
+
}
|
|
2482
|
+
: {
|
|
2477
2483
|
...SAFE_ENV,
|
|
2478
|
-
|
|
2479
|
-
|
|
2484
|
+
CLEMENTINE_1M_CONTEXT_MODE: oneMillionModeValue,
|
|
2485
|
+
...(oneMillionDisableValue !== undefined
|
|
2486
|
+
? { CLAUDE_CODE_DISABLE_1M_CONTEXT: oneMillionDisableValue }
|
|
2480
2487
|
: {}),
|
|
2481
2488
|
},
|
|
2482
|
-
}),
|
|
2483
2489
|
// Avoid ambient Claude Code user/project/local settings and plugins by
|
|
2484
2490
|
// default. Those can silently attach hundreds of tools. Explicit MCP
|
|
2485
2491
|
// servers above still work; "all integrations/full tool surface" keeps
|
|
@@ -3421,13 +3427,14 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3421
3427
|
responseText = responseText || ('Claude says the account credit balance is too low. I paused background jobs for a few hours so they stop draining/retrying, but interactive chat will also fail until credits are available again.');
|
|
3422
3428
|
}
|
|
3423
3429
|
else if (looksLikeOneMillionContextError(errorText)) {
|
|
3430
|
+
process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
|
|
3424
3431
|
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
3425
3432
|
if (sessionKey) {
|
|
3426
3433
|
this.sessions.delete(sessionKey);
|
|
3427
3434
|
this.exchangeCounts.set(sessionKey, 0);
|
|
3428
3435
|
this._compactedSessions.delete(sessionKey);
|
|
3429
3436
|
}
|
|
3430
|
-
responseText = responseText || ("Claude rejected
|
|
3437
|
+
responseText = responseText || ("Claude rejected 1M context for this account. I've switched this process to 200K recovery mode and reset the session. To persist the fix across restarts, run `clementine budgets safe`, then `clementine restart`.");
|
|
3431
3438
|
}
|
|
3432
3439
|
else if (lower.includes('rate') && lower.includes('limit')) {
|
|
3433
3440
|
hitRateLimit = true;
|
|
@@ -3552,13 +3559,14 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3552
3559
|
responseText = responseText || ('Claude says the account credit balance is too low. I paused background jobs for a few hours so they stop draining/retrying, but interactive chat will also fail until credits are available again.');
|
|
3553
3560
|
}
|
|
3554
3561
|
else if (looksLikeOneMillionContextError(e)) {
|
|
3562
|
+
process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
|
|
3555
3563
|
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
3556
3564
|
if (sessionKey) {
|
|
3557
3565
|
this.sessions.delete(sessionKey);
|
|
3558
3566
|
this.exchangeCounts.set(sessionKey, 0);
|
|
3559
3567
|
this._compactedSessions.delete(sessionKey);
|
|
3560
3568
|
}
|
|
3561
|
-
responseText = responseText || ("Claude rejected
|
|
3569
|
+
responseText = responseText || ("Claude rejected 1M context for this account. I've switched this process to 200K recovery mode and reset the session. To persist the fix across restarts, run `clementine budgets safe`, then `clementine restart`.");
|
|
3562
3570
|
}
|
|
3563
3571
|
else if (errStr.includes('rate') && (errStr.includes('limit') || errStr.includes('rate_limit'))) {
|
|
3564
3572
|
hitRateLimit = true;
|
|
@@ -4814,6 +4822,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4814
4822
|
throw new Error(errText);
|
|
4815
4823
|
}
|
|
4816
4824
|
if (looksLikeOneMillionContextError(errText)) {
|
|
4825
|
+
process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
|
|
4817
4826
|
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
4818
4827
|
throw new Error(errText);
|
|
4819
4828
|
}
|
|
@@ -5162,6 +5171,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
5162
5171
|
throw new Error(exitText);
|
|
5163
5172
|
}
|
|
5164
5173
|
if (looksLikeOneMillionContextError(exitText)) {
|
|
5174
|
+
process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
|
|
5165
5175
|
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
5166
5176
|
throw new Error(exitText);
|
|
5167
5177
|
}
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -5459,6 +5459,8 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5459
5459
|
label: 'Model',
|
|
5460
5460
|
keys: [
|
|
5461
5461
|
{ key: 'DEFAULT_MODEL_TIER', label: 'Default Tier', hint: 'haiku, sonnet, or opus', type: 'select:haiku,sonnet,opus' },
|
|
5462
|
+
{ key: 'CLEMENTINE_1M_CONTEXT_MODE', label: '1M Context Mode', hint: 'auto allows included Opus 1M where available; off forces 200K; on forces 1M', type: 'select:auto,off,on' },
|
|
5463
|
+
{ key: 'CLAUDE_CODE_DISABLE_1M_CONTEXT', label: 'Legacy 1M Disable', hint: 'Backward-compatible Claude Code switch. Prefer CLEMENTINE_1M_CONTEXT_MODE.', type: 'select:,1,0,true,false' },
|
|
5462
5464
|
],
|
|
5463
5465
|
},
|
|
5464
5466
|
{
|
|
@@ -5585,6 +5587,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5585
5587
|
return result;
|
|
5586
5588
|
}
|
|
5587
5589
|
function writeEnvValue(key, value) {
|
|
5590
|
+
mkdirSync(BASE_DIR, { recursive: true });
|
|
5588
5591
|
let content = existsSync(ENV_PATH) ? readFileSync(ENV_PATH, 'utf-8') : '';
|
|
5589
5592
|
const re = new RegExp(`^${key}=.*$`, 'm');
|
|
5590
5593
|
if (re.test(content)) {
|
|
@@ -5593,7 +5596,72 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5593
5596
|
else {
|
|
5594
5597
|
content = content.trimEnd() + `\n${key}=${value}\n`;
|
|
5595
5598
|
}
|
|
5596
|
-
writeFileSync(ENV_PATH, content);
|
|
5599
|
+
writeFileSync(ENV_PATH, content, { mode: 0o600 });
|
|
5600
|
+
}
|
|
5601
|
+
function deleteEnvValue(key) {
|
|
5602
|
+
if (!existsSync(ENV_PATH))
|
|
5603
|
+
return;
|
|
5604
|
+
const re = new RegExp(`^${key}=.*\n?`, 'm');
|
|
5605
|
+
const content = readFileSync(ENV_PATH, 'utf-8').replace(re, '');
|
|
5606
|
+
writeFileSync(ENV_PATH, content, { mode: 0o600 });
|
|
5607
|
+
}
|
|
5608
|
+
const DASHBOARD_BUDGET_ROWS = [
|
|
5609
|
+
{ key: 'BUDGET_CHAT_USD', value: '5', label: 'Chat', hint: 'Per interactive chat turn' },
|
|
5610
|
+
{ key: 'BUDGET_HEARTBEAT_USD', value: '0.25', label: 'Heartbeat', hint: 'Per proactive heartbeat tick' },
|
|
5611
|
+
{ key: 'BUDGET_CRON_T1_USD', value: '0.75', label: 'Tier 1 cron', hint: 'Per lightweight scheduled job' },
|
|
5612
|
+
{ key: 'BUDGET_CRON_T2_USD', value: '1.5', label: 'Tier 2 cron', hint: 'Per deeper scheduled job' },
|
|
5613
|
+
];
|
|
5614
|
+
const SAFE_DASHBOARD_BUDGETS = [
|
|
5615
|
+
{ key: 'BUDGET_HEARTBEAT_USD', value: '0.25', label: 'Heartbeat' },
|
|
5616
|
+
{ key: 'BUDGET_CRON_T1_USD', value: '0.75', label: 'Tier 1 cron' },
|
|
5617
|
+
{ key: 'BUDGET_CRON_T2_USD', value: '1.5', label: 'Tier 2 cron' },
|
|
5618
|
+
];
|
|
5619
|
+
const DASHBOARD_BUDGET_KEYS = new Set(DASHBOARD_BUDGET_ROWS.map(row => row.key));
|
|
5620
|
+
function normalizeDashboardOneMillionMode(value) {
|
|
5621
|
+
const v = String(value ?? '').trim().toLowerCase();
|
|
5622
|
+
if (!v)
|
|
5623
|
+
return null;
|
|
5624
|
+
if (v === 'auto')
|
|
5625
|
+
return 'auto';
|
|
5626
|
+
if (['off', 'disable', 'disabled', 'safe', '200k', 'standard'].includes(v))
|
|
5627
|
+
return 'off';
|
|
5628
|
+
if (['on', 'enable', 'enabled', 'yes', 'true', '1'].includes(v))
|
|
5629
|
+
return 'on';
|
|
5630
|
+
return null;
|
|
5631
|
+
}
|
|
5632
|
+
function legacyDisableToDashboardMode(value) {
|
|
5633
|
+
const v = String(value ?? '').trim().toLowerCase();
|
|
5634
|
+
if (!v)
|
|
5635
|
+
return null;
|
|
5636
|
+
if (['1', 'true', 'yes', 'on'].includes(v))
|
|
5637
|
+
return 'off';
|
|
5638
|
+
if (['0', 'false', 'no', 'off'].includes(v))
|
|
5639
|
+
return 'on';
|
|
5640
|
+
return null;
|
|
5641
|
+
}
|
|
5642
|
+
function formatDashboardBudgetValue(value) {
|
|
5643
|
+
const n = Number(value);
|
|
5644
|
+
if (!Number.isFinite(n))
|
|
5645
|
+
return String(value ?? '');
|
|
5646
|
+
if (n === 0)
|
|
5647
|
+
return 'No cap';
|
|
5648
|
+
return `$${n.toFixed(2)}`;
|
|
5649
|
+
}
|
|
5650
|
+
function writeDashboardBudgetCap(key, value) {
|
|
5651
|
+
if (!DASHBOARD_BUDGET_KEYS.has(key)) {
|
|
5652
|
+
return { ok: false, error: 'Unknown budget key' };
|
|
5653
|
+
}
|
|
5654
|
+
const n = Number(value);
|
|
5655
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
5656
|
+
return { ok: false, error: 'Budget must be a non-negative dollar amount. Use 0 for no cap.' };
|
|
5657
|
+
}
|
|
5658
|
+
if (n > 1000) {
|
|
5659
|
+
return { ok: false, error: 'Budget cap is too high for the dashboard. Use the CLI if you really need a cap above $1000.' };
|
|
5660
|
+
}
|
|
5661
|
+
const normalized = n === 0 ? '0' : String(Math.round(n * 100) / 100);
|
|
5662
|
+
writeEnvValue(key, normalized);
|
|
5663
|
+
process.env[key] = normalized;
|
|
5664
|
+
return { ok: true, value: normalized };
|
|
5597
5665
|
}
|
|
5598
5666
|
const ASSISTANT_PREF_OPTIONS = {
|
|
5599
5667
|
proactivity: ['quiet', 'balanced', 'proactive', 'operator'],
|
|
@@ -5653,6 +5721,188 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5653
5721
|
res.status(500).json({ error: String(err) });
|
|
5654
5722
|
}
|
|
5655
5723
|
});
|
|
5724
|
+
app.get('/api/budgets', async (_req, res) => {
|
|
5725
|
+
try {
|
|
5726
|
+
const [{ computeEffectiveConfig }, { runDoctor }] = await Promise.all([
|
|
5727
|
+
import('../config/effective-config.js'),
|
|
5728
|
+
import('../config/config-doctor.js'),
|
|
5729
|
+
]);
|
|
5730
|
+
const cfg = computeEffectiveConfig(BASE_DIR);
|
|
5731
|
+
const doctor = runDoctor(BASE_DIR);
|
|
5732
|
+
const byKey = new Map(cfg.entries.map(e => [e.key, e]));
|
|
5733
|
+
const oneMModeEntry = byKey.get('CLEMENTINE_1M_CONTEXT_MODE');
|
|
5734
|
+
const legacyEntry = byKey.get('CLAUDE_CODE_DISABLE_1M_CONTEXT');
|
|
5735
|
+
const legacyMode = legacyDisableToDashboardMode(legacyEntry?.value);
|
|
5736
|
+
const mode = normalizeDashboardOneMillionMode(oneMModeEntry?.value)
|
|
5737
|
+
?? legacyMode
|
|
5738
|
+
?? 'auto';
|
|
5739
|
+
const source = oneMModeEntry?.source !== 'default'
|
|
5740
|
+
? oneMModeEntry?.source
|
|
5741
|
+
: legacyMode
|
|
5742
|
+
? legacyEntry?.source ?? 'default'
|
|
5743
|
+
: oneMModeEntry?.source ?? 'default';
|
|
5744
|
+
const summary = mode === 'off'
|
|
5745
|
+
? 'Recovery mode: all models stay on standard 200K context.'
|
|
5746
|
+
: mode === 'on'
|
|
5747
|
+
? 'Forced 1M: Sonnet and Pro subscriptions may require Claude Extra Usage.'
|
|
5748
|
+
: 'Smart auto: Opus can use included 1M on eligible Max/Team/Enterprise accounts; Sonnet stays on 200K.';
|
|
5749
|
+
const relevantKeys = new Set([
|
|
5750
|
+
'BUDGET_CHAT_USD',
|
|
5751
|
+
'BUDGET_HEARTBEAT_USD',
|
|
5752
|
+
'BUDGET_CRON_T1_USD',
|
|
5753
|
+
'BUDGET_CRON_T2_USD',
|
|
5754
|
+
'CLEMENTINE_1M_CONTEXT_MODE',
|
|
5755
|
+
'CLAUDE_CODE_DISABLE_1M_CONTEXT',
|
|
5756
|
+
]);
|
|
5757
|
+
const findings = doctor.findings
|
|
5758
|
+
.filter(f => (f.key && relevantKeys.has(f.key)) || /budget|credit|1m|context/i.test(f.message))
|
|
5759
|
+
.slice(0, 8);
|
|
5760
|
+
res.json({
|
|
5761
|
+
ok: true,
|
|
5762
|
+
baseDir: cfg.baseDir,
|
|
5763
|
+
budgets: DASHBOARD_BUDGET_ROWS.map(row => {
|
|
5764
|
+
const entry = byKey.get(row.key);
|
|
5765
|
+
return {
|
|
5766
|
+
label: row.label,
|
|
5767
|
+
hint: row.hint,
|
|
5768
|
+
key: row.key,
|
|
5769
|
+
value: entry?.value ?? '',
|
|
5770
|
+
displayValue: formatDashboardBudgetValue(entry?.value),
|
|
5771
|
+
source: entry?.source ?? 'unknown',
|
|
5772
|
+
};
|
|
5773
|
+
}),
|
|
5774
|
+
context: {
|
|
5775
|
+
mode,
|
|
5776
|
+
source,
|
|
5777
|
+
summary,
|
|
5778
|
+
modeValue: oneMModeEntry?.value ?? 'auto',
|
|
5779
|
+
legacyValue: legacyEntry?.value ?? '',
|
|
5780
|
+
legacySource: legacyEntry?.source ?? 'default',
|
|
5781
|
+
legacyMode,
|
|
5782
|
+
},
|
|
5783
|
+
findings,
|
|
5784
|
+
counts: doctor.counts,
|
|
5785
|
+
});
|
|
5786
|
+
}
|
|
5787
|
+
catch (err) {
|
|
5788
|
+
res.status(500).json({ error: String(err) });
|
|
5789
|
+
}
|
|
5790
|
+
});
|
|
5791
|
+
app.post('/api/budgets/set', (req, res) => {
|
|
5792
|
+
try {
|
|
5793
|
+
const body = req.body;
|
|
5794
|
+
const key = String(body?.key ?? '');
|
|
5795
|
+
const result = writeDashboardBudgetCap(key, body?.value);
|
|
5796
|
+
if (!result.ok) {
|
|
5797
|
+
res.status(400).json({ error: result.error });
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5800
|
+
res.json({
|
|
5801
|
+
ok: true,
|
|
5802
|
+
message: `${key} set to ${formatDashboardBudgetValue(result.value)}. Restart Clementine to apply to running workers.`,
|
|
5803
|
+
});
|
|
5804
|
+
}
|
|
5805
|
+
catch (err) {
|
|
5806
|
+
res.status(500).json({ error: String(err) });
|
|
5807
|
+
}
|
|
5808
|
+
});
|
|
5809
|
+
app.post('/api/budgets/preset', (req, res) => {
|
|
5810
|
+
try {
|
|
5811
|
+
const preset = String(req.body?.preset ?? '').trim().toLowerCase();
|
|
5812
|
+
let writes;
|
|
5813
|
+
let message;
|
|
5814
|
+
if (preset === 'defaults' || preset === 'standard') {
|
|
5815
|
+
writes = DASHBOARD_BUDGET_ROWS.map(row => ({ key: row.key, value: row.value }));
|
|
5816
|
+
message = 'Restored the standard spend caps. Restart Clementine to apply to running workers.';
|
|
5817
|
+
}
|
|
5818
|
+
else if (preset === 'uncapped' || preset === 'off' || preset === 'none') {
|
|
5819
|
+
writes = DASHBOARD_BUDGET_ROWS.map(row => ({ key: row.key, value: '0' }));
|
|
5820
|
+
message = 'Removed spend caps by setting all budget values to 0. Restart Clementine to apply to running workers.';
|
|
5821
|
+
}
|
|
5822
|
+
else {
|
|
5823
|
+
res.status(400).json({ error: 'preset must be defaults or uncapped' });
|
|
5824
|
+
return;
|
|
5825
|
+
}
|
|
5826
|
+
for (const item of writes) {
|
|
5827
|
+
const result = writeDashboardBudgetCap(item.key, item.value);
|
|
5828
|
+
if (!result.ok) {
|
|
5829
|
+
res.status(400).json({ error: result.error });
|
|
5830
|
+
return;
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5833
|
+
res.json({ ok: true, message });
|
|
5834
|
+
}
|
|
5835
|
+
catch (err) {
|
|
5836
|
+
res.status(500).json({ error: String(err) });
|
|
5837
|
+
}
|
|
5838
|
+
});
|
|
5839
|
+
app.post('/api/budgets/safe', (_req, res) => {
|
|
5840
|
+
try {
|
|
5841
|
+
for (const item of SAFE_DASHBOARD_BUDGETS) {
|
|
5842
|
+
const result = writeDashboardBudgetCap(item.key, item.value);
|
|
5843
|
+
if (!result.ok) {
|
|
5844
|
+
res.status(400).json({ error: result.error });
|
|
5845
|
+
return;
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
writeEnvValue('CLEMENTINE_1M_CONTEXT_MODE', 'off');
|
|
5849
|
+
writeEnvValue('CLAUDE_CODE_DISABLE_1M_CONTEXT', '1');
|
|
5850
|
+
process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
|
|
5851
|
+
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
5852
|
+
res.json({
|
|
5853
|
+
ok: true,
|
|
5854
|
+
message: 'Applied safe budgets and 200K recovery mode. Restart Clementine to apply to running chat workers.',
|
|
5855
|
+
});
|
|
5856
|
+
}
|
|
5857
|
+
catch (err) {
|
|
5858
|
+
res.status(500).json({ error: String(err) });
|
|
5859
|
+
}
|
|
5860
|
+
});
|
|
5861
|
+
app.post('/api/budgets/1m', (req, res) => {
|
|
5862
|
+
try {
|
|
5863
|
+
const mode = normalizeDashboardOneMillionMode(req.body?.mode);
|
|
5864
|
+
if (!mode) {
|
|
5865
|
+
res.status(400).json({ error: 'mode must be auto, off, or on' });
|
|
5866
|
+
return;
|
|
5867
|
+
}
|
|
5868
|
+
writeEnvValue('CLEMENTINE_1M_CONTEXT_MODE', mode);
|
|
5869
|
+
process.env.CLEMENTINE_1M_CONTEXT_MODE = mode;
|
|
5870
|
+
if (mode === 'auto') {
|
|
5871
|
+
deleteEnvValue('CLAUDE_CODE_DISABLE_1M_CONTEXT');
|
|
5872
|
+
delete process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT;
|
|
5873
|
+
}
|
|
5874
|
+
else {
|
|
5875
|
+
const legacyValue = mode === 'on' ? '0' : '1';
|
|
5876
|
+
writeEnvValue('CLAUDE_CODE_DISABLE_1M_CONTEXT', legacyValue);
|
|
5877
|
+
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = legacyValue;
|
|
5878
|
+
}
|
|
5879
|
+
const message = mode === 'auto'
|
|
5880
|
+
? 'Set 1M context to smart auto. Restart Clementine to apply everywhere.'
|
|
5881
|
+
: mode === 'off'
|
|
5882
|
+
? 'Disabled 1M context for recovery. Restart Clementine to apply everywhere.'
|
|
5883
|
+
: 'Forced 1M context on. Sonnet and Pro subscriptions may require Claude Extra Usage.';
|
|
5884
|
+
res.json({ ok: true, message });
|
|
5885
|
+
}
|
|
5886
|
+
catch (err) {
|
|
5887
|
+
res.status(500).json({ error: String(err) });
|
|
5888
|
+
}
|
|
5889
|
+
});
|
|
5890
|
+
app.post('/api/budgets/doctor-fix', async (_req, res) => {
|
|
5891
|
+
try {
|
|
5892
|
+
const { applyDoctorFixes } = await import('../config/config-doctor.js');
|
|
5893
|
+
const result = applyDoctorFixes(BASE_DIR);
|
|
5894
|
+
for (const item of result.changed) {
|
|
5895
|
+
process.env[item.key] = item.value;
|
|
5896
|
+
}
|
|
5897
|
+
const message = result.changed.length
|
|
5898
|
+
? `Applied ${result.changed.length} config fix${result.changed.length === 1 ? '' : 'es'}. Restart Clementine to apply everywhere.`
|
|
5899
|
+
: 'No budget or context fixes were needed.';
|
|
5900
|
+
res.json({ ok: true, message, result });
|
|
5901
|
+
}
|
|
5902
|
+
catch (err) {
|
|
5903
|
+
res.status(500).json({ error: String(err) });
|
|
5904
|
+
}
|
|
5905
|
+
});
|
|
5656
5906
|
app.get('/api/settings', (_req, res) => {
|
|
5657
5907
|
try {
|
|
5658
5908
|
const env = parseEnvFile();
|
|
@@ -5739,10 +5989,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5739
5989
|
res.status(404).json({ error: '.env file not found' });
|
|
5740
5990
|
return;
|
|
5741
5991
|
}
|
|
5742
|
-
|
|
5743
|
-
const re = new RegExp(`^${key}=.*\n?`, 'm');
|
|
5744
|
-
content = content.replace(re, '');
|
|
5745
|
-
writeFileSync(ENV_PATH, content);
|
|
5992
|
+
deleteEnvValue(key);
|
|
5746
5993
|
// Hot-reload mirror of the PUT handler — drop process.env entry +
|
|
5747
5994
|
// reset Composio client so removal takes effect without a restart.
|
|
5748
5995
|
if (key === 'COMPOSIO_API_KEY' || key === 'COMPOSIO_USER_ID') {
|
|
@@ -15169,6 +15416,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15169
15416
|
<p style="color:var(--text-muted);margin:0">Manage API keys and configuration. Changes are saved to <code>~/.clementine/.env</code>.</p>
|
|
15170
15417
|
<button class="btn-sm" style="white-space:nowrap;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-primary);padding:6px 12px;border-radius:6px;cursor:pointer" onclick="restartDashboard()">Restart Dashboard</button>
|
|
15171
15418
|
</div>
|
|
15419
|
+
<div id="budget-health-content" style="margin-bottom:16px"><div class="empty-state">Loading budget health...</div></div>
|
|
15172
15420
|
<div id="settings-content"><div class="empty-state">Loading settings...</div></div>
|
|
15173
15421
|
</div>
|
|
15174
15422
|
<div class="tab-pane" id="tab-settings-remote">
|
|
@@ -19717,8 +19965,153 @@ async function listenToDigest() {
|
|
|
19717
19965
|
} catch(e) { toast(String(e), 'error'); }
|
|
19718
19966
|
}
|
|
19719
19967
|
|
|
19968
|
+
async function refreshBudgetHealth() {
|
|
19969
|
+
var container = document.getElementById('budget-health-content');
|
|
19970
|
+
if (!container) return;
|
|
19971
|
+
try {
|
|
19972
|
+
var r = await apiFetch('/api/budgets');
|
|
19973
|
+
var d = await r.json();
|
|
19974
|
+
if (!d.ok) {
|
|
19975
|
+
container.innerHTML = '<div class="empty-state" style="color:var(--red)">Failed to load budget health: ' + esc(d.error || 'Unknown error') + '</div>';
|
|
19976
|
+
return;
|
|
19977
|
+
}
|
|
19978
|
+
var context = d.context || {};
|
|
19979
|
+
var mode = context.mode || 'auto';
|
|
19980
|
+
var modeClass = mode === 'off' ? 'badge-green' : mode === 'on' ? 'badge-yellow' : 'badge-blue';
|
|
19981
|
+
var rows = d.budgets || [];
|
|
19982
|
+
var findings = d.findings || [];
|
|
19983
|
+
var html = '<div class="card">'
|
|
19984
|
+
+ '<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:12px">'
|
|
19985
|
+
+ '<div style="display:flex;align-items:center;gap:8px"><span>Spend Guards & Context Health</span><span class="badge ' + modeClass + '" style="font-size:10px">1M ' + esc(mode) + '</span></div>'
|
|
19986
|
+
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">'
|
|
19987
|
+
+ '<button class="btn-sm btn-primary" onclick="applySafeBudgetPreset()">Safe Recovery</button>'
|
|
19988
|
+
+ '<button class="btn-sm" onclick="applyBudgetPreset(\\x27defaults\\x27)">Default Caps</button>'
|
|
19989
|
+
+ '<button class="btn-sm" onclick="applyBudgetPreset(\\x27uncapped\\x27)">No Caps</button>'
|
|
19990
|
+
+ '<button class="btn-sm" onclick="setBudgetContextMode(\\x27auto\\x27)">Smart Auto</button>'
|
|
19991
|
+
+ '<button class="btn-sm" onclick="setBudgetContextMode(\\x27off\\x27)">Force 200K</button>'
|
|
19992
|
+
+ '<button class="btn-sm" onclick="forceBudgetOneMillion()">Force 1M</button>'
|
|
19993
|
+
+ '<button class="btn-sm" onclick="applyBudgetDoctorFix()">Doctor Fix</button>'
|
|
19994
|
+
+ '</div></div>'
|
|
19995
|
+
+ '<div class="card-body" style="padding:16px">';
|
|
19996
|
+
html += '<div style="font-size:12px;color:var(--text-secondary);margin-bottom:12px">Spend guards are per-run dollar caps. They prevent runaway background work, but setting a cap to 0 removes that guard.</div>';
|
|
19997
|
+
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-bottom:12px">';
|
|
19998
|
+
for (var i = 0; i < rows.length; i++) {
|
|
19999
|
+
var row = rows[i];
|
|
20000
|
+
var inputId = 'budget-cap-' + String(row.key).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
20001
|
+
var rawValue = Number(row.value);
|
|
20002
|
+
var inputValue = Number.isFinite(rawValue) ? String(rawValue) : '';
|
|
20003
|
+
html += '<div style="border:1px solid var(--border);border-radius:8px;padding:10px;background:var(--bg-secondary)">'
|
|
20004
|
+
+ '<div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">'
|
|
20005
|
+
+ '<div><div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0">' + esc(row.label) + '</div>'
|
|
20006
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">' + esc(row.hint || row.key) + '</div></div>'
|
|
20007
|
+
+ '<span class="badge badge-gray" style="font-size:10px">' + esc(row.source || 'unknown') + '</span>'
|
|
20008
|
+
+ '</div>'
|
|
20009
|
+
+ '<div style="display:flex;gap:6px;align-items:center;margin-top:10px">'
|
|
20010
|
+
+ '<span style="font-size:13px;color:var(--text-muted)">$</span>'
|
|
20011
|
+
+ '<input id="' + inputId + '" type="number" min="0" step="0.05" value="' + esc(inputValue) + '" data-budget-key="' + esc(row.key) + '"'
|
|
20012
|
+
+ ' style="flex:1;min-width:0;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-primary);color:var(--text-primary);font-size:13px">'
|
|
20013
|
+
+ '<button class="btn-sm" onclick="saveBudgetCap(\\x27' + esc(row.key) + '\\x27)">Save</button>'
|
|
20014
|
+
+ '</div>'
|
|
20015
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:6px">' + esc(row.key) + ' - ' + esc(row.displayValue || row.value || '') + '</div>'
|
|
20016
|
+
+ '</div>';
|
|
20017
|
+
}
|
|
20018
|
+
html += '</div>';
|
|
20019
|
+
html += '<div style="display:flex;gap:10px;align-items:flex-start;flex-wrap:wrap;margin-bottom:12px">'
|
|
20020
|
+
+ '<div style="flex:1;min-width:240px;border:1px solid var(--border);border-radius:8px;padding:10px;background:var(--bg-primary)">'
|
|
20021
|
+
+ '<div style="font-weight:600;font-size:13px">1M context mode</div>'
|
|
20022
|
+
+ '<div style="font-size:12px;color:var(--text-secondary);margin-top:4px">' + esc(context.summary || '') + '</div>'
|
|
20023
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:6px">Source: ' + esc(context.source || 'default')
|
|
20024
|
+
+ (context.legacyValue ? ' | legacy CLAUDE_CODE_DISABLE_1M_CONTEXT=' + esc(context.legacyValue) + ' from ' + esc(context.legacySource || 'default') : '')
|
|
20025
|
+
+ '</div></div>'
|
|
20026
|
+
+ '<div style="flex:1;min-width:240px;border:1px solid var(--border);border-radius:8px;padding:10px;background:var(--bg-primary)">'
|
|
20027
|
+
+ '<div style="font-weight:600;font-size:13px">What this protects</div>'
|
|
20028
|
+
+ '<div style="font-size:12px;color:var(--text-secondary);margin-top:4px">Safe Recovery lowers autonomous spend and disables 1M context for accounts seeing credit or entitlement errors.</div>'
|
|
20029
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:6px">Restart the daemon after changing budgets or context mode.</div>'
|
|
20030
|
+
+ '</div></div>';
|
|
20031
|
+
if (findings.length) {
|
|
20032
|
+
html += '<div style="border-top:1px solid var(--border);padding-top:10px">'
|
|
20033
|
+
+ '<div style="font-weight:600;font-size:13px;margin-bottom:6px">Potential causes</div>';
|
|
20034
|
+
for (var j = 0; j < findings.length; j++) {
|
|
20035
|
+
var f = findings[j];
|
|
20036
|
+
var cls = f.severity === 'error' ? 'badge-red' : f.severity === 'warning' ? 'badge-yellow' : 'badge-gray';
|
|
20037
|
+
html += '<div style="display:flex;gap:8px;align-items:flex-start;padding:6px 0;border-bottom:1px solid rgba(127,127,127,0.12)">'
|
|
20038
|
+
+ '<span class="badge ' + cls + '" style="font-size:10px;min-width:54px;text-align:center">' + esc(f.severity || 'info') + '</span>'
|
|
20039
|
+
+ '<div style="font-size:12px;color:var(--text-secondary);line-height:1.4">'
|
|
20040
|
+
+ (f.key ? '<code>' + esc(f.key) + '</code>: ' : '') + esc(f.message || '')
|
|
20041
|
+
+ (f.fix ? '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">Fix: <code>' + esc(f.fix) + '</code></div>' : '')
|
|
20042
|
+
+ '</div></div>';
|
|
20043
|
+
}
|
|
20044
|
+
html += '</div>';
|
|
20045
|
+
} else {
|
|
20046
|
+
html += '<div style="font-size:12px;color:var(--text-muted);border-top:1px solid var(--border);padding-top:10px">No budget or context warnings found.</div>';
|
|
20047
|
+
}
|
|
20048
|
+
html += '</div></div>';
|
|
20049
|
+
container.innerHTML = html;
|
|
20050
|
+
} catch(e) {
|
|
20051
|
+
container.innerHTML = '<div class="empty-state" style="color:var(--red)">Failed to load budget health: ' + esc(String(e)) + '</div>';
|
|
20052
|
+
}
|
|
20053
|
+
}
|
|
20054
|
+
|
|
20055
|
+
async function postBudgetAction(url, body) {
|
|
20056
|
+
try {
|
|
20057
|
+
var r = await apiFetch(url, {
|
|
20058
|
+
method: 'POST',
|
|
20059
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20060
|
+
body: JSON.stringify(body || {}),
|
|
20061
|
+
});
|
|
20062
|
+
var d = await r.json();
|
|
20063
|
+
if (d.ok) toast(d.message || 'Updated', 'success');
|
|
20064
|
+
else toast(d.error || 'Error', 'error');
|
|
20065
|
+
await refreshBudgetHealth();
|
|
20066
|
+
return d;
|
|
20067
|
+
} catch(e) {
|
|
20068
|
+
toast(String(e), 'error');
|
|
20069
|
+
return null;
|
|
20070
|
+
}
|
|
20071
|
+
}
|
|
20072
|
+
|
|
20073
|
+
async function applySafeBudgetPreset() {
|
|
20074
|
+
await postBudgetAction('/api/budgets/safe', {});
|
|
20075
|
+
refreshSettings();
|
|
20076
|
+
}
|
|
20077
|
+
|
|
20078
|
+
async function applyBudgetPreset(preset) {
|
|
20079
|
+
if (preset === 'uncapped' && !confirm('Remove all spend caps? Clementine can still hit account limits or credits if a job runs long.')) return;
|
|
20080
|
+
await postBudgetAction('/api/budgets/preset', { preset: preset });
|
|
20081
|
+
refreshSettings();
|
|
20082
|
+
}
|
|
20083
|
+
|
|
20084
|
+
async function saveBudgetCap(key) {
|
|
20085
|
+
var inputId = 'budget-cap-' + String(key).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
20086
|
+
var input = document.getElementById(inputId);
|
|
20087
|
+
if (!input) return;
|
|
20088
|
+
var value = Number(input.value);
|
|
20089
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
20090
|
+
toast('Budget must be a non-negative dollar amount. Use 0 for no cap.', 'error');
|
|
20091
|
+
return;
|
|
20092
|
+
}
|
|
20093
|
+
await postBudgetAction('/api/budgets/set', { key: key, value: value });
|
|
20094
|
+
refreshSettings();
|
|
20095
|
+
}
|
|
20096
|
+
|
|
20097
|
+
async function setBudgetContextMode(mode) {
|
|
20098
|
+
await postBudgetAction('/api/budgets/1m', { mode: mode });
|
|
20099
|
+
refreshSettings();
|
|
20100
|
+
}
|
|
20101
|
+
|
|
20102
|
+
async function forceBudgetOneMillion() {
|
|
20103
|
+
if (!confirm('Force 1M context on? Sonnet and Pro subscriptions may require Claude Extra Usage.')) return;
|
|
20104
|
+
await setBudgetContextMode('on');
|
|
20105
|
+
}
|
|
20106
|
+
|
|
20107
|
+
async function applyBudgetDoctorFix() {
|
|
20108
|
+
await postBudgetAction('/api/budgets/doctor-fix', {});
|
|
20109
|
+
refreshSettings();
|
|
20110
|
+
}
|
|
20111
|
+
|
|
19720
20112
|
async function refreshSettings() {
|
|
19721
20113
|
var container = document.getElementById('settings-content');
|
|
20114
|
+
refreshBudgetHealth();
|
|
19722
20115
|
try {
|
|
19723
20116
|
var r = await apiFetch('/api/settings');
|
|
19724
20117
|
var d = await r.json();
|
package/dist/cli/index.js
CHANGED
|
@@ -959,6 +959,14 @@ function upsertEnvValue(key, value) {
|
|
|
959
959
|
// harden-permissions`.
|
|
960
960
|
writeFileSync(ENV_PATH, content, { mode: 0o600 });
|
|
961
961
|
}
|
|
962
|
+
function removeEnvValue(key) {
|
|
963
|
+
if (!existsSync(ENV_PATH))
|
|
964
|
+
return;
|
|
965
|
+
const upperKey = key.toUpperCase();
|
|
966
|
+
const content = readFileSync(ENV_PATH, 'utf-8');
|
|
967
|
+
const lines = content.split(/\r?\n/).filter(line => !new RegExp(`^${upperKey}=`).test(line));
|
|
968
|
+
writeFileSync(ENV_PATH, lines.join('\n').trimEnd() + '\n', { mode: 0o600 });
|
|
969
|
+
}
|
|
962
970
|
function cmdConfigSet(key, value) {
|
|
963
971
|
const upperKey = key.toUpperCase();
|
|
964
972
|
upsertEnvValue(upperKey, value);
|
|
@@ -1033,8 +1041,27 @@ const BUDGET_ALIASES = {
|
|
|
1033
1041
|
'cron-t2': 'BUDGET_CRON_T2_USD',
|
|
1034
1042
|
t2: 'BUDGET_CRON_T2_USD',
|
|
1035
1043
|
};
|
|
1036
|
-
function
|
|
1037
|
-
|
|
1044
|
+
function normalizeOneMillionCliMode(value) {
|
|
1045
|
+
const v = String(value ?? '').trim().toLowerCase();
|
|
1046
|
+
if (!v)
|
|
1047
|
+
return null;
|
|
1048
|
+
if (v === 'auto')
|
|
1049
|
+
return 'auto';
|
|
1050
|
+
if (['off', 'disable', 'disabled', 'safe', '200k', 'standard'].includes(v))
|
|
1051
|
+
return 'off';
|
|
1052
|
+
if (['on', 'enable', 'enabled', 'yes', 'true', '1'].includes(v))
|
|
1053
|
+
return 'on';
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
function legacyDisableToOneMillionCliMode(value) {
|
|
1057
|
+
const v = String(value ?? '').trim().toLowerCase();
|
|
1058
|
+
if (!v)
|
|
1059
|
+
return null;
|
|
1060
|
+
if (['1', 'true', 'yes', 'on'].includes(v))
|
|
1061
|
+
return 'off';
|
|
1062
|
+
if (['0', 'false', 'no', 'off'].includes(v))
|
|
1063
|
+
return 'on';
|
|
1064
|
+
return null;
|
|
1038
1065
|
}
|
|
1039
1066
|
function normalizeBudgetKey(name) {
|
|
1040
1067
|
const raw = name.trim();
|
|
@@ -1084,20 +1111,33 @@ async function cmdBudgetsShow() {
|
|
|
1084
1111
|
const source = entry?.source ?? 'unknown';
|
|
1085
1112
|
console.log(` ${label.padEnd(12)} ${BOLD}${formatBudgetValue(entry?.value).padEnd(8)}${RESET} ${DIM}${key} from ${source}${RESET}`);
|
|
1086
1113
|
}
|
|
1114
|
+
const oneMModeEntry = byKey.get('CLEMENTINE_1M_CONTEXT_MODE');
|
|
1115
|
+
const persistedOneMMode = readPersistedEnvValue('CLEMENTINE_1M_CONTEXT_MODE');
|
|
1116
|
+
const oneMMode = normalizeOneMillionCliMode(persistedOneMMode ?? oneMModeEntry?.value)
|
|
1117
|
+
?? 'auto';
|
|
1118
|
+
const oneMModeSource = persistedOneMMode !== undefined ? '.env' : oneMModeEntry?.source ?? 'default';
|
|
1087
1119
|
const oneM = byKey.get('CLAUDE_CODE_DISABLE_1M_CONTEXT');
|
|
1088
1120
|
const persistedOneM = readPersistedEnvValue('CLAUDE_CODE_DISABLE_1M_CONTEXT');
|
|
1089
1121
|
const oneMValue = persistedOneM ?? oneM?.value;
|
|
1090
1122
|
const oneMSource = persistedOneM !== undefined ? '.env' : oneM?.source ?? 'unknown';
|
|
1091
|
-
const
|
|
1123
|
+
const legacyMode = legacyDisableToOneMillionCliMode(oneMValue);
|
|
1092
1124
|
console.log();
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1125
|
+
const modeColor = oneMMode === 'off' ? GREEN : oneMMode === 'on' ? YELLOW : BOLD;
|
|
1126
|
+
console.log(` 1M context ${modeColor}${oneMMode}${RESET} ${DIM}CLEMENTINE_1M_CONTEXT_MODE=${String(persistedOneMMode ?? oneMModeEntry?.value ?? 'auto')} from ${oneMModeSource}${RESET}`);
|
|
1127
|
+
if (legacyMode && oneMModeEntry?.source === 'default') {
|
|
1128
|
+
console.log(` ${DIM}legacy CLAUDE_CODE_DISABLE_1M_CONTEXT=${String(oneMValue ?? '')} from ${oneMSource} maps to mode=${legacyMode}${RESET}`);
|
|
1129
|
+
}
|
|
1130
|
+
if (oneMMode === 'auto') {
|
|
1131
|
+
console.log(` ${DIM}allows included Opus 1M on Max/Team/Enterprise; keeps Sonnet on 200K unless mode=on${RESET}`);
|
|
1132
|
+
}
|
|
1133
|
+
else if (oneMMode === 'on') {
|
|
1134
|
+
console.log(` ${YELLOW}Note:${RESET} forced 1M requires Extra Usage for Sonnet and for Pro subscriptions.`);
|
|
1096
1135
|
}
|
|
1097
1136
|
console.log();
|
|
1098
1137
|
console.log(` ${DIM}Useful commands:${RESET}`);
|
|
1099
|
-
console.log(` clementine budgets safe ${DIM}lower background budgets and
|
|
1100
|
-
console.log(` clementine budgets 1m
|
|
1138
|
+
console.log(` clementine budgets safe ${DIM}lower background budgets and force 200K context${RESET}`);
|
|
1139
|
+
console.log(` clementine budgets 1m auto ${DIM}allow included Opus 1M, keep Sonnet safe${RESET}`);
|
|
1140
|
+
console.log(` clementine budgets 1m on ${DIM}force 1M context for Extra Usage/API users${RESET}`);
|
|
1101
1141
|
console.log(` clementine budgets 1m off ${DIM}disable 1M context for maximum compatibility${RESET}`);
|
|
1102
1142
|
console.log(` clementine budgets set chat 10 ${DIM}raise one budget cap${RESET}`);
|
|
1103
1143
|
console.log();
|
|
@@ -1113,6 +1153,7 @@ async function cmdBudgetsSafe() {
|
|
|
1113
1153
|
const RESET = '\x1b[0m';
|
|
1114
1154
|
const writes = [
|
|
1115
1155
|
...SAFE_BACKGROUND_BUDGETS,
|
|
1156
|
+
{ key: 'CLEMENTINE_1M_CONTEXT_MODE', value: 'off', label: '1M context mode' },
|
|
1116
1157
|
{ key: 'CLAUDE_CODE_DISABLE_1M_CONTEXT', value: '1', label: '1M context disabled' },
|
|
1117
1158
|
];
|
|
1118
1159
|
for (const item of writes) {
|
|
@@ -1134,23 +1175,34 @@ function cmdBudgetsOneMillion(mode) {
|
|
|
1134
1175
|
const normalized = mode.trim().toLowerCase();
|
|
1135
1176
|
const on = new Set(['on', 'enable', 'enabled', 'yes', 'true', '1']);
|
|
1136
1177
|
const off = new Set(['off', 'disable', 'disabled', 'no', 'false', '0']);
|
|
1137
|
-
|
|
1138
|
-
|
|
1178
|
+
const auto = new Set(['auto', 'smart', 'default']);
|
|
1179
|
+
if (!on.has(normalized) && !off.has(normalized) && !auto.has(normalized)) {
|
|
1180
|
+
console.error(' Usage: clementine budgets 1m <auto|on|off>');
|
|
1139
1181
|
process.exitCode = 1;
|
|
1140
1182
|
return;
|
|
1141
1183
|
}
|
|
1184
|
+
if (auto.has(normalized)) {
|
|
1185
|
+
upsertEnvValue('CLEMENTINE_1M_CONTEXT_MODE', 'auto');
|
|
1186
|
+
removeEnvValue('CLAUDE_CODE_DISABLE_1M_CONTEXT');
|
|
1187
|
+
console.log();
|
|
1188
|
+
console.log(' Set Claude 1M context mode to auto.');
|
|
1189
|
+
console.log(' Opus can use included 1M on Max/Team/Enterprise; Sonnet stays on 200K unless you force mode=on. Restart Clementine to apply.');
|
|
1190
|
+
console.log();
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1142
1193
|
const enable = on.has(normalized);
|
|
1194
|
+
upsertEnvValue('CLEMENTINE_1M_CONTEXT_MODE', enable ? 'on' : 'off');
|
|
1143
1195
|
upsertEnvValue('CLAUDE_CODE_DISABLE_1M_CONTEXT', enable ? '0' : '1');
|
|
1144
1196
|
if (enable) {
|
|
1145
1197
|
console.log();
|
|
1146
|
-
console.log('
|
|
1147
|
-
console.log(' Requires
|
|
1198
|
+
console.log(' Forced Claude 1M context on for Clementine.');
|
|
1199
|
+
console.log(' Requires Extra Usage for Sonnet and Pro subscriptions, or API/PAYG billing. Restart Clementine to apply.');
|
|
1148
1200
|
console.log();
|
|
1149
1201
|
}
|
|
1150
1202
|
else {
|
|
1151
1203
|
console.log();
|
|
1152
1204
|
console.log(' Disabled Claude 1M context for Clementine.');
|
|
1153
|
-
console.log(' This is the safest
|
|
1205
|
+
console.log(' This is the safest recovery mode for users hitting 1M entitlement errors. Restart Clementine to apply.');
|
|
1154
1206
|
console.log();
|
|
1155
1207
|
}
|
|
1156
1208
|
}
|
|
@@ -2368,13 +2420,13 @@ budgetsCmd
|
|
|
2368
2420
|
});
|
|
2369
2421
|
budgetsCmd
|
|
2370
2422
|
.command('safe')
|
|
2371
|
-
.description('Apply the stable local-safe preset: lower background budgets and
|
|
2423
|
+
.description('Apply the stable local-safe preset: lower background budgets and force 200K context')
|
|
2372
2424
|
.action(async () => {
|
|
2373
2425
|
await cmdBudgetsSafe();
|
|
2374
2426
|
});
|
|
2375
2427
|
budgetsCmd
|
|
2376
2428
|
.command('1m <mode>')
|
|
2377
|
-
.description('
|
|
2429
|
+
.description('Set Claude 1M context mode for Clementine (auto | on | off)')
|
|
2378
2430
|
.action(cmdBudgetsOneMillion);
|
|
2379
2431
|
budgetsCmd
|
|
2380
2432
|
.command('set <name> <value>')
|
|
@@ -38,6 +38,7 @@ const NUMERIC_KEYS = new Set([
|
|
|
38
38
|
const ENUM_KEYS = {
|
|
39
39
|
CLEMENTINE_ADVISOR_RULES_LOADER: ['off', 'shadow', 'primary'],
|
|
40
40
|
DEFAULT_MODEL_TIER: ['haiku', 'sonnet', 'opus'],
|
|
41
|
+
CLEMENTINE_1M_CONTEXT_MODE: ['auto', 'off', 'on'],
|
|
41
42
|
WEBHOOK_ENABLED: ['true', 'false'],
|
|
42
43
|
ALLOW_ALL_USERS: ['true', 'false'],
|
|
43
44
|
CLEMENTINE_ALLOW_SOURCE_EDITS: ['true', 'false', '1', '0', 'yes', 'no'],
|
|
@@ -109,9 +110,11 @@ export function applyDoctorFixes(baseDir) {
|
|
|
109
110
|
}
|
|
110
111
|
changed.push({ key, value, reason });
|
|
111
112
|
};
|
|
113
|
+
const oneMMode = byKey.get('CLEMENTINE_1M_CONTEXT_MODE');
|
|
112
114
|
const oneM = byKey.get('CLAUDE_CODE_DISABLE_1M_CONTEXT');
|
|
113
|
-
if (oneM && isFalseyToggle(oneM.value)) {
|
|
114
|
-
setSafeValue('
|
|
115
|
+
if ((oneMMode && String(oneMMode.value).toLowerCase() === 'on') || (oneM && isFalseyToggle(oneM.value))) {
|
|
116
|
+
setSafeValue('CLEMENTINE_1M_CONTEXT_MODE', 'off', 'Force standard 200K context until the account is confirmed stable.');
|
|
117
|
+
setSafeValue('CLAUDE_CODE_DISABLE_1M_CONTEXT', '1', 'Disable Claude Code 1M context for backward compatibility with older Claude Code versions.');
|
|
115
118
|
}
|
|
116
119
|
for (const [key, recommended] of Object.entries(SAFE_BACKGROUND_DEFAULTS)) {
|
|
117
120
|
const entry = byKey.get(key);
|
|
@@ -273,16 +276,28 @@ function checkRangeSanity(cfg, findings) {
|
|
|
273
276
|
function checkOperationalOverrides(cfg, baseDir, findings) {
|
|
274
277
|
const byKey = new Map(cfg.entries.map(e => [e.key, e]));
|
|
275
278
|
const persistedEnv = readEnvValues(baseDir);
|
|
279
|
+
const oneMMode = byKey.get('CLEMENTINE_1M_CONTEXT_MODE');
|
|
280
|
+
if (oneMMode && oneMMode.source !== 'default' && String(oneMMode.value).toLowerCase() === 'on') {
|
|
281
|
+
findings.push({
|
|
282
|
+
severity: 'warning',
|
|
283
|
+
key: 'CLEMENTINE_1M_CONTEXT_MODE',
|
|
284
|
+
message: `1M context is forced on from ${oneMMode.source}. Sonnet 1M requires Claude Extra Usage, and Pro subscriptions require Extra Usage for both Sonnet and Opus 1M.`,
|
|
285
|
+
fix: 'clementine budgets 1m auto # smart mode, or clementine budgets safe for recovery',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
276
288
|
const oneM = byKey.get('CLAUDE_CODE_DISABLE_1M_CONTEXT');
|
|
277
|
-
if (oneM
|
|
289
|
+
if (oneM
|
|
290
|
+
&& oneM.source !== 'default'
|
|
291
|
+
&& isFalseyToggle(oneM.value)
|
|
292
|
+
&& (!oneMMode || oneMMode.source === 'default')) {
|
|
278
293
|
const source = oneM.source === 'process.env' && persistedEnv.CLAUDE_CODE_DISABLE_1M_CONTEXT !== undefined
|
|
279
294
|
? '.env'
|
|
280
295
|
: oneM.source;
|
|
281
296
|
findings.push({
|
|
282
297
|
severity: 'warning',
|
|
283
298
|
key: 'CLAUDE_CODE_DISABLE_1M_CONTEXT',
|
|
284
|
-
message: `1M context is explicitly enabled from ${source}.
|
|
285
|
-
fix: 'clementine
|
|
299
|
+
message: `Legacy 1M context is explicitly enabled from ${source}. Sonnet 1M requires Extra Usage and can fail calls with "Extra usage is required for 1M context."`,
|
|
300
|
+
fix: 'clementine budgets 1m auto # smart mode, or clementine budgets safe for recovery',
|
|
286
301
|
});
|
|
287
302
|
}
|
|
288
303
|
for (const [key, recommended] of Object.entries(SAFE_BACKGROUND_DEFAULTS)) {
|
|
@@ -30,7 +30,8 @@ const SPECS = [
|
|
|
30
30
|
{ key: 'HAIKU_MODEL', group: 'models', jsonPath: 'models.haiku', default: 'claude-haiku-4-5-20251001' },
|
|
31
31
|
{ key: 'SONNET_MODEL', group: 'models', jsonPath: 'models.sonnet', default: 'claude-sonnet-4-6' },
|
|
32
32
|
{ key: 'OPUS_MODEL', group: 'models', jsonPath: 'models.opus', default: 'claude-opus-4-7' },
|
|
33
|
-
{ key: '
|
|
33
|
+
{ key: 'CLEMENTINE_1M_CONTEXT_MODE', group: 'models', default: 'auto' },
|
|
34
|
+
{ key: 'CLAUDE_CODE_DISABLE_1M_CONTEXT', group: 'models', default: '' },
|
|
34
35
|
// Team routing
|
|
35
36
|
{ key: 'AUTO_DELEGATE_ENABLED', group: 'team', default: false },
|
|
36
37
|
// Budgets
|
package/dist/config.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export declare const PKG_DIR: string;
|
|
|
11
11
|
/** Data home — user data, vault, .env, logs, sessions. */
|
|
12
12
|
export declare const BASE_DIR: string;
|
|
13
13
|
import { shellEscape as _shellEscape } from './config/env-parser.js';
|
|
14
|
+
export type OneMillionContextMode = 'auto' | 'off' | 'on';
|
|
15
|
+
export declare const CLEMENTINE_1M_CONTEXT_MODE: OneMillionContextMode;
|
|
16
|
+
export declare function currentOneMillionContextMode(): OneMillionContextMode;
|
|
17
|
+
export declare function claudeCodeDisableOneMillionForModel(model: string | null | undefined, mode?: OneMillionContextMode): '1' | '0' | undefined;
|
|
18
|
+
export declare function normalizeClaudeModelForOneMillionContext(model: string, mode?: OneMillionContextMode): string;
|
|
19
|
+
export declare function usesOneMillionContext(model: string | null | undefined, mode?: OneMillionContextMode): boolean;
|
|
14
20
|
/**
|
|
15
21
|
* Look up a config value: local .env first, then process.env fallback.
|
|
16
22
|
* Keychain refs in either source are resolved lazily; failed resolution
|
package/dist/config.js
CHANGED
|
@@ -27,31 +27,84 @@ function readEnvFile() {
|
|
|
27
27
|
return parseEnvText(readFileSync(envPath, 'utf-8'));
|
|
28
28
|
}
|
|
29
29
|
const env = readEnvFile();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
30
|
+
function normalizeOneMillionContextMode(value) {
|
|
31
|
+
const v = String(value ?? '').trim().toLowerCase();
|
|
32
|
+
if (!v)
|
|
33
|
+
return null;
|
|
34
|
+
if (v === 'auto')
|
|
35
|
+
return 'auto';
|
|
36
|
+
if (['off', 'disable', 'disabled', 'safe', '200k', 'standard'].includes(v))
|
|
37
|
+
return 'off';
|
|
38
|
+
if (['on', 'enable', 'enabled', 'yes', 'true', '1'].includes(v))
|
|
39
|
+
return 'on';
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function legacyDisableToOneMillionContextMode(value) {
|
|
43
|
+
const v = String(value ?? '').trim().toLowerCase();
|
|
44
|
+
if (!v)
|
|
45
|
+
return null;
|
|
46
|
+
if (['1', 'true', 'yes', 'on'].includes(v))
|
|
47
|
+
return 'off';
|
|
48
|
+
if (['0', 'false', 'no', 'off'].includes(v))
|
|
49
|
+
return 'on';
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const oneMillionModePref = env['CLEMENTINE_1M_CONTEXT_MODE'] ?? process.env.CLEMENTINE_1M_CONTEXT_MODE;
|
|
53
|
+
const legacyOneMillionPref = env['CLAUDE_CODE_DISABLE_1M_CONTEXT'] ?? process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT;
|
|
54
|
+
export const CLEMENTINE_1M_CONTEXT_MODE = normalizeOneMillionContextMode(oneMillionModePref)
|
|
55
|
+
?? legacyDisableToOneMillionContextMode(legacyOneMillionPref)
|
|
56
|
+
?? 'auto';
|
|
57
|
+
if (CLEMENTINE_1M_CONTEXT_MODE === 'off') {
|
|
58
|
+
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
59
|
+
}
|
|
60
|
+
else if (CLEMENTINE_1M_CONTEXT_MODE === 'on') {
|
|
61
|
+
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '0';
|
|
62
|
+
}
|
|
63
|
+
else if (oneMillionModePref !== undefined) {
|
|
64
|
+
// Explicit auto mode should override stale shell/package env inherited from
|
|
65
|
+
// older installs. Per-query SDK options decide whether to disable 1M.
|
|
66
|
+
delete process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT;
|
|
67
|
+
}
|
|
68
|
+
function modelFamily(model) {
|
|
69
|
+
const m = String(model ?? '').toLowerCase();
|
|
70
|
+
if (m.includes('opusplan'))
|
|
71
|
+
return 'opusplan';
|
|
72
|
+
if (m.includes('opus'))
|
|
73
|
+
return 'opus';
|
|
74
|
+
if (m.includes('sonnet'))
|
|
75
|
+
return 'sonnet';
|
|
76
|
+
if (m.includes('haiku'))
|
|
77
|
+
return 'haiku';
|
|
78
|
+
return 'other';
|
|
79
|
+
}
|
|
80
|
+
export function currentOneMillionContextMode() {
|
|
81
|
+
return normalizeOneMillionContextMode(process.env.CLEMENTINE_1M_CONTEXT_MODE)
|
|
82
|
+
?? CLEMENTINE_1M_CONTEXT_MODE;
|
|
83
|
+
}
|
|
84
|
+
export function claudeCodeDisableOneMillionForModel(model, mode = currentOneMillionContextMode()) {
|
|
85
|
+
if (mode === 'off')
|
|
86
|
+
return '1';
|
|
87
|
+
if (mode === 'on')
|
|
88
|
+
return '0';
|
|
89
|
+
return modelFamily(model) === 'opus' ? undefined : '1';
|
|
90
|
+
}
|
|
91
|
+
export function normalizeClaudeModelForOneMillionContext(model, mode = currentOneMillionContextMode()) {
|
|
92
|
+
if (mode === 'on')
|
|
93
|
+
return model;
|
|
94
|
+
const family = modelFamily(model);
|
|
95
|
+
const shouldStrip = mode === 'off'
|
|
96
|
+
|| family === 'sonnet'
|
|
97
|
+
|| family === 'haiku'
|
|
98
|
+
|| family === 'opusplan';
|
|
99
|
+
return shouldStrip ? model.replace(/\[1m\]/ig, '') : model;
|
|
100
|
+
}
|
|
101
|
+
export function usesOneMillionContext(model, mode = currentOneMillionContextMode()) {
|
|
102
|
+
if (mode === 'off')
|
|
103
|
+
return false;
|
|
104
|
+
const family = modelFamily(model);
|
|
105
|
+
if (mode === 'on')
|
|
106
|
+
return family === 'opus' || family === 'sonnet';
|
|
107
|
+
return family === 'opus';
|
|
55
108
|
}
|
|
56
109
|
// ── Keychain-ref resolution (lazy, cached) ──────────────────────────
|
|
57
110
|
//
|