clementine-agent 1.18.15 → 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 +5 -0
- package/dist/cli/dashboard.js +119 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -324,6 +324,10 @@ 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
|
|
@@ -333,6 +337,7 @@ clementine budgets 1m auto # allow included Opus 1M, keep Sonnet on 200K
|
|
|
333
337
|
clementine budgets 1m on # force 1M context for Extra Usage/API users
|
|
334
338
|
clementine budgets 1m off # disable 1M context for maximum compatibility
|
|
335
339
|
clementine budgets set chat 10 # raise one budget cap
|
|
340
|
+
clementine budgets set chat 0 # remove one cap
|
|
336
341
|
```
|
|
337
342
|
|
|
338
343
|
**Commonly tuned knobs:**
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -5605,11 +5605,18 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5605
5605
|
const content = readFileSync(ENV_PATH, 'utf-8').replace(re, '');
|
|
5606
5606
|
writeFileSync(ENV_PATH, content, { mode: 0o600 });
|
|
5607
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
|
+
];
|
|
5608
5614
|
const SAFE_DASHBOARD_BUDGETS = [
|
|
5609
5615
|
{ key: 'BUDGET_HEARTBEAT_USD', value: '0.25', label: 'Heartbeat' },
|
|
5610
5616
|
{ key: 'BUDGET_CRON_T1_USD', value: '0.75', label: 'Tier 1 cron' },
|
|
5611
5617
|
{ key: 'BUDGET_CRON_T2_USD', value: '1.5', label: 'Tier 2 cron' },
|
|
5612
5618
|
];
|
|
5619
|
+
const DASHBOARD_BUDGET_KEYS = new Set(DASHBOARD_BUDGET_ROWS.map(row => row.key));
|
|
5613
5620
|
function normalizeDashboardOneMillionMode(value) {
|
|
5614
5621
|
const v = String(value ?? '').trim().toLowerCase();
|
|
5615
5622
|
if (!v)
|
|
@@ -5636,8 +5643,26 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5636
5643
|
const n = Number(value);
|
|
5637
5644
|
if (!Number.isFinite(n))
|
|
5638
5645
|
return String(value ?? '');
|
|
5646
|
+
if (n === 0)
|
|
5647
|
+
return 'No cap';
|
|
5639
5648
|
return `$${n.toFixed(2)}`;
|
|
5640
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 };
|
|
5665
|
+
}
|
|
5641
5666
|
const ASSISTANT_PREF_OPTIONS = {
|
|
5642
5667
|
proactivity: ['quiet', 'balanced', 'proactive', 'operator'],
|
|
5643
5668
|
responseStyle: ['concise', 'balanced', 'detailed'],
|
|
@@ -5705,12 +5730,6 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5705
5730
|
const cfg = computeEffectiveConfig(BASE_DIR);
|
|
5706
5731
|
const doctor = runDoctor(BASE_DIR);
|
|
5707
5732
|
const byKey = new Map(cfg.entries.map(e => [e.key, e]));
|
|
5708
|
-
const rows = [
|
|
5709
|
-
{ label: 'Chat', key: 'BUDGET_CHAT_USD' },
|
|
5710
|
-
{ label: 'Heartbeat', key: 'BUDGET_HEARTBEAT_USD' },
|
|
5711
|
-
{ label: 'Tier 1 cron', key: 'BUDGET_CRON_T1_USD' },
|
|
5712
|
-
{ label: 'Tier 2 cron', key: 'BUDGET_CRON_T2_USD' },
|
|
5713
|
-
];
|
|
5714
5733
|
const oneMModeEntry = byKey.get('CLEMENTINE_1M_CONTEXT_MODE');
|
|
5715
5734
|
const legacyEntry = byKey.get('CLAUDE_CODE_DISABLE_1M_CONTEXT');
|
|
5716
5735
|
const legacyMode = legacyDisableToDashboardMode(legacyEntry?.value);
|
|
@@ -5741,10 +5760,11 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5741
5760
|
res.json({
|
|
5742
5761
|
ok: true,
|
|
5743
5762
|
baseDir: cfg.baseDir,
|
|
5744
|
-
budgets:
|
|
5763
|
+
budgets: DASHBOARD_BUDGET_ROWS.map(row => {
|
|
5745
5764
|
const entry = byKey.get(row.key);
|
|
5746
5765
|
return {
|
|
5747
5766
|
label: row.label,
|
|
5767
|
+
hint: row.hint,
|
|
5748
5768
|
key: row.key,
|
|
5749
5769
|
value: entry?.value ?? '',
|
|
5750
5770
|
displayValue: formatDashboardBudgetValue(entry?.value),
|
|
@@ -5768,11 +5788,62 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5768
5788
|
res.status(500).json({ error: String(err) });
|
|
5769
5789
|
}
|
|
5770
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
|
+
});
|
|
5771
5839
|
app.post('/api/budgets/safe', (_req, res) => {
|
|
5772
5840
|
try {
|
|
5773
5841
|
for (const item of SAFE_DASHBOARD_BUDGETS) {
|
|
5774
|
-
|
|
5775
|
-
|
|
5842
|
+
const result = writeDashboardBudgetCap(item.key, item.value);
|
|
5843
|
+
if (!result.ok) {
|
|
5844
|
+
res.status(400).json({ error: result.error });
|
|
5845
|
+
return;
|
|
5846
|
+
}
|
|
5776
5847
|
}
|
|
5777
5848
|
writeEnvValue('CLEMENTINE_1M_CONTEXT_MODE', 'off');
|
|
5778
5849
|
writeEnvValue('CLAUDE_CODE_DISABLE_1M_CONTEXT', '1');
|
|
@@ -19911,22 +19982,37 @@ async function refreshBudgetHealth() {
|
|
|
19911
19982
|
var findings = d.findings || [];
|
|
19912
19983
|
var html = '<div class="card">'
|
|
19913
19984
|
+ '<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:12px">'
|
|
19914
|
-
+ '<div style="display:flex;align-items:center;gap:8px"><span>
|
|
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>'
|
|
19915
19986
|
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">'
|
|
19916
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>'
|
|
19917
19990
|
+ '<button class="btn-sm" onclick="setBudgetContextMode(\\x27auto\\x27)">Smart Auto</button>'
|
|
19918
19991
|
+ '<button class="btn-sm" onclick="setBudgetContextMode(\\x27off\\x27)">Force 200K</button>'
|
|
19919
19992
|
+ '<button class="btn-sm" onclick="forceBudgetOneMillion()">Force 1M</button>'
|
|
19920
19993
|
+ '<button class="btn-sm" onclick="applyBudgetDoctorFix()">Doctor Fix</button>'
|
|
19921
19994
|
+ '</div></div>'
|
|
19922
19995
|
+ '<div class="card-body" style="padding:16px">';
|
|
19923
|
-
html += '<div style="
|
|
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">';
|
|
19924
19998
|
for (var i = 0; i < rows.length; i++) {
|
|
19925
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) : '';
|
|
19926
20003
|
html += '<div style="border:1px solid var(--border);border-radius:8px;padding:10px;background:var(--bg-secondary)">'
|
|
19927
|
-
+ '<div style="
|
|
19928
|
-
+ '<div style="font-
|
|
19929
|
-
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">' + esc(row.
|
|
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>'
|
|
19930
20016
|
+ '</div>';
|
|
19931
20017
|
}
|
|
19932
20018
|
html += '</div>';
|
|
@@ -19989,6 +20075,25 @@ async function applySafeBudgetPreset() {
|
|
|
19989
20075
|
refreshSettings();
|
|
19990
20076
|
}
|
|
19991
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
|
+
|
|
19992
20097
|
async function setBudgetContextMode(mode) {
|
|
19993
20098
|
await postBudgetAction('/api/budgets/1m', { mode: mode });
|
|
19994
20099
|
refreshSettings();
|