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 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:**
@@ -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: rows.map(row => {
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
- writeEnvValue(item.key, item.value);
5775
- process.env[item.key] = item.value;
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>Budget &amp; Context Health</span><span class="badge ' + modeClass + '" style="font-size:10px">1M ' + esc(mode) + '</span></div>'
19985
+ + '<div style="display:flex;align-items:center;gap:8px"><span>Spend Guards &amp; 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="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-bottom:12px">';
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="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0">' + esc(row.label) + '</div>'
19928
- + '<div style="font-weight:700;font-size:18px;margin-top:4px">' + esc(row.displayValue || row.value || '') + '</div>'
19929
- + '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">' + esc(row.key) + ' from ' + esc(row.source || 'unknown') + '</div>'
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.15",
3
+ "version": "1.18.16",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",