clementine-agent 1.18.15 → 1.18.17

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,12 @@ 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. When a dashboard change needs the
330
+ daemon to reload, Clementine shows a Restart Clementine prompt and handles the
331
+ restart from the browser.
332
+
327
333
  For spend/context tuning, `clementine budgets` gives a safer shortcut:
328
334
 
329
335
  ```bash
@@ -333,6 +339,7 @@ clementine budgets 1m auto # allow included Opus 1M, keep Sonnet on 200K
333
339
  clementine budgets 1m on # force 1M context for Extra Usage/API users
334
340
  clementine budgets 1m off # disable 1M context for maximum compatibility
335
341
  clementine budgets set chat 10 # raise one budget cap
342
+ clementine budgets set chat 0 # remove one cap
336
343
  ```
337
344
 
338
345
  **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');
@@ -15343,7 +15414,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
15343
15414
  <div class="tab-pane active" id="tab-settings-general">
15344
15415
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
15345
15416
  <p style="color:var(--text-muted);margin:0">Manage API keys and configuration. Changes are saved to <code>~/.clementine/.env</code>.</p>
15346
- <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>
15417
+ <button class="btn-sm btn-primary" style="white-space:nowrap;padding:6px 12px;border-radius:6px;cursor:pointer" onclick="restartDaemonFromDashboard()">Restart Clementine</button>
15347
15418
  </div>
15348
15419
  <div id="budget-health-content" style="margin-bottom:16px"><div class="empty-state">Loading budget health...</div></div>
15349
15420
  <div id="settings-content"><div class="empty-state">Loading settings...</div></div>
@@ -15508,8 +15579,8 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
15508
15579
  <div class="card" style="margin-bottom:16px">
15509
15580
  <div class="card-header">Diagnostics &amp; maintenance</div>
15510
15581
  <div class="card-body" style="padding:16px;display:flex;gap:8px;flex-wrap:wrap">
15582
+ <button class="btn-sm btn-primary" onclick="restartDaemonFromDashboard()">Restart Clementine</button>
15511
15583
  <button class="btn-sm" onclick="restartDashboard()">Restart Dashboard</button>
15512
- <button class="btn-sm" onclick="if(confirm('Restart the daemon? Active sessions drain first.')) apiPost('/api/restart')">Restart Daemon</button>
15513
15584
  <button class="btn-sm" onclick="apiFetch('/api/doctor').then(function(r){return r.text()}).then(function(t){alert(t)})">Run Doctor</button>
15514
15585
  <button class="btn-sm" onclick="apiFetch('/api/version').then(function(r){return r.json()}).then(function(d){alert('Version: '+(d.version||'?')+'\\nNode: '+(d.node||'?'))})">Build info</button>
15515
15586
  </div>
@@ -17562,6 +17633,7 @@ async function apiPost(url) {
17562
17633
  if (d.ok) toast(d.message, 'success');
17563
17634
  else toast(d.error || 'Error', 'error');
17564
17635
  setTimeout(refreshAll, 1000);
17636
+ return d;
17565
17637
  } catch(e) { toast(String(e), 'error'); }
17566
17638
  }
17567
17639
  async function apiJson(method, url, body) {
@@ -17585,9 +17657,84 @@ async function apiDelete(url) {
17585
17657
  if (d.ok) toast(d.message, 'success');
17586
17658
  else toast(d.error || 'Error', 'error');
17587
17659
  setTimeout(refreshAll, 500);
17660
+ return d;
17588
17661
  } catch(e) { toast(String(e), 'error'); }
17589
17662
  }
17590
17663
 
17664
+ function settingRequiresDaemonRestart(key) {
17665
+ if (!key) return true;
17666
+ if (key === 'COMPOSIO_API_KEY' || key === 'COMPOSIO_USER_ID') return false;
17667
+ if (key.indexOf('ASSISTANT_') === 0) return false;
17668
+ return true;
17669
+ }
17670
+
17671
+ function renderRestartRequiredBanner() {
17672
+ var reason = '';
17673
+ try { reason = localStorage.getItem('clem-restart-required') || ''; } catch(e) { reason = ''; }
17674
+ var existing = document.getElementById('restart-required-banner');
17675
+ if (!reason) {
17676
+ if (existing) existing.remove();
17677
+ return;
17678
+ }
17679
+ if (!existing) {
17680
+ existing = document.createElement('div');
17681
+ existing.id = 'restart-required-banner';
17682
+ existing.style.cssText = 'position:fixed;left:18px;right:18px;bottom:18px;z-index:9999;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);box-shadow:0 8px 28px rgba(0,0,0,0.28);padding:12px 14px;color:var(--text-primary)';
17683
+ document.body.appendChild(existing);
17684
+ }
17685
+ existing.innerHTML = '<div style="min-width:220px;flex:1"><div style="font-weight:700;font-size:13px">Restart required</div>'
17686
+ + '<div style="font-size:12px;color:var(--text-secondary);margin-top:2px">' + esc(reason) + '</div></div>'
17687
+ + '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
17688
+ + '<button class="btn-sm btn-primary" onclick="restartDaemonFromDashboard()">Restart Clementine</button>'
17689
+ + '<button class="btn-sm" onclick="dismissRestartRequiredBanner()">Later</button>'
17690
+ + '</div>';
17691
+ }
17692
+
17693
+ function markRestartRequired(reason) {
17694
+ var msg = reason || 'This change needs a Clementine restart before the daemon and channel workers use it.';
17695
+ try { localStorage.setItem('clem-restart-required', msg); } catch(e) { /* ignore */ }
17696
+ renderRestartRequiredBanner();
17697
+ }
17698
+
17699
+ function clearRestartRequired() {
17700
+ try { localStorage.removeItem('clem-restart-required'); } catch(e) { /* ignore */ }
17701
+ var existing = document.getElementById('restart-required-banner');
17702
+ if (existing) existing.remove();
17703
+ }
17704
+
17705
+ function dismissRestartRequiredBanner() {
17706
+ var existing = document.getElementById('restart-required-banner');
17707
+ if (existing) existing.remove();
17708
+ }
17709
+
17710
+ async function restartDaemonFromDashboard(skipConfirm) {
17711
+ if (!skipConfirm && !confirm('Restart Clementine now? Active work may pause briefly while the daemon reloads.')) return;
17712
+ toast('Restarting Clementine...', 'info');
17713
+ try {
17714
+ var r = await apiFetch('/api/restart', { method: 'POST' });
17715
+ var d = {};
17716
+ try { d = await r.json(); } catch(e) { d = {}; }
17717
+ if (!r.ok || d.error) {
17718
+ var err = String(d.error || 'Restart failed');
17719
+ if (/not running/i.test(err)) {
17720
+ var launch = await apiFetch('/api/launch', { method: 'POST' });
17721
+ var launchData = await launch.json();
17722
+ if (!launch.ok || launchData.error) throw new Error(launchData.error || 'Launch failed');
17723
+ clearRestartRequired();
17724
+ toast('Clementine started', 'success');
17725
+ setTimeout(refreshAll, 2000);
17726
+ return;
17727
+ }
17728
+ throw new Error(err);
17729
+ }
17730
+ clearRestartRequired();
17731
+ toast('Clementine restart requested', 'success');
17732
+ setTimeout(refreshAll, 2500);
17733
+ } catch(e) {
17734
+ toast('Restart failed: ' + String(e), 'error');
17735
+ }
17736
+ }
17737
+
17591
17738
  // ── Status + Overview ─────────────────────
17592
17739
  let lastStatusData = {};
17593
17740
  async function refreshStatus(preloaded) {
@@ -18738,6 +18885,7 @@ async function saveHeartbeatControl() {
18738
18885
  if (!r.ok || d.error) throw new Error(d.error || 'Save failed');
18739
18886
  if (statusEl) { statusEl.textContent = 'Saved. Restart daemon for schedule changes.'; statusEl.style.color = 'var(--green)'; }
18740
18887
  toast('Heartbeat controls saved', 'success');
18888
+ markRestartRequired('Heartbeat control changes need a Clementine restart before the schedule uses them.');
18741
18889
  } catch(e) {
18742
18890
  if (statusEl) { statusEl.textContent = 'Save failed'; statusEl.style.color = 'var(--red)'; }
18743
18891
  toast(String(e), 'error');
@@ -19911,22 +20059,37 @@ async function refreshBudgetHealth() {
19911
20059
  var findings = d.findings || [];
19912
20060
  var html = '<div class="card">'
19913
20061
  + '<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>'
20062
+ + '<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
20063
  + '<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">'
19916
20064
  + '<button class="btn-sm btn-primary" onclick="applySafeBudgetPreset()">Safe Recovery</button>'
20065
+ + '<button class="btn-sm" onclick="applyBudgetPreset(\\x27defaults\\x27)">Default Caps</button>'
20066
+ + '<button class="btn-sm" onclick="applyBudgetPreset(\\x27uncapped\\x27)">No Caps</button>'
19917
20067
  + '<button class="btn-sm" onclick="setBudgetContextMode(\\x27auto\\x27)">Smart Auto</button>'
19918
20068
  + '<button class="btn-sm" onclick="setBudgetContextMode(\\x27off\\x27)">Force 200K</button>'
19919
20069
  + '<button class="btn-sm" onclick="forceBudgetOneMillion()">Force 1M</button>'
19920
20070
  + '<button class="btn-sm" onclick="applyBudgetDoctorFix()">Doctor Fix</button>'
19921
20071
  + '</div></div>'
19922
20072
  + '<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">';
20073
+ 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>';
20074
+ html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-bottom:12px">';
19924
20075
  for (var i = 0; i < rows.length; i++) {
19925
20076
  var row = rows[i];
20077
+ var inputId = 'budget-cap-' + String(row.key).replace(/[^A-Za-z0-9_-]/g, '-');
20078
+ var rawValue = Number(row.value);
20079
+ var inputValue = Number.isFinite(rawValue) ? String(rawValue) : '';
19926
20080
  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>'
20081
+ + '<div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">'
20082
+ + '<div><div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0">' + esc(row.label) + '</div>'
20083
+ + '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">' + esc(row.hint || row.key) + '</div></div>'
20084
+ + '<span class="badge badge-gray" style="font-size:10px">' + esc(row.source || 'unknown') + '</span>'
20085
+ + '</div>'
20086
+ + '<div style="display:flex;gap:6px;align-items:center;margin-top:10px">'
20087
+ + '<span style="font-size:13px;color:var(--text-muted)">$</span>'
20088
+ + '<input id="' + inputId + '" type="number" min="0" step="0.05" value="' + esc(inputValue) + '" data-budget-key="' + esc(row.key) + '"'
20089
+ + ' 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">'
20090
+ + '<button class="btn-sm" onclick="saveBudgetCap(\\x27' + esc(row.key) + '\\x27)">Save</button>'
20091
+ + '</div>'
20092
+ + '<div style="font-size:11px;color:var(--text-muted);margin-top:6px">' + esc(row.key) + ' - ' + esc(row.displayValue || row.value || '') + '</div>'
19930
20093
  + '</div>';
19931
20094
  }
19932
20095
  html += '</div>';
@@ -19985,12 +20148,35 @@ async function postBudgetAction(url, body) {
19985
20148
  }
19986
20149
 
19987
20150
  async function applySafeBudgetPreset() {
19988
- await postBudgetAction('/api/budgets/safe', {});
20151
+ var d = await postBudgetAction('/api/budgets/safe', {});
20152
+ if (d && d.ok) markRestartRequired('Safe Recovery changed spend/context settings. Restart Clementine to apply them to chat and background workers.');
20153
+ refreshSettings();
20154
+ }
20155
+
20156
+ async function applyBudgetPreset(preset) {
20157
+ if (preset === 'uncapped' && !confirm('Remove all spend caps? Clementine can still hit account limits or credits if a job runs long.')) return;
20158
+ var d = await postBudgetAction('/api/budgets/preset', { preset: preset });
20159
+ if (d && d.ok) markRestartRequired('Spend guard changes need a Clementine restart before running workers use the new caps.');
20160
+ refreshSettings();
20161
+ }
20162
+
20163
+ async function saveBudgetCap(key) {
20164
+ var inputId = 'budget-cap-' + String(key).replace(/[^A-Za-z0-9_-]/g, '-');
20165
+ var input = document.getElementById(inputId);
20166
+ if (!input) return;
20167
+ var value = Number(input.value);
20168
+ if (!Number.isFinite(value) || value < 0) {
20169
+ toast('Budget must be a non-negative dollar amount. Use 0 for no cap.', 'error');
20170
+ return;
20171
+ }
20172
+ var d = await postBudgetAction('/api/budgets/set', { key: key, value: value });
20173
+ if (d && d.ok) markRestartRequired('Budget cap changes need a Clementine restart before running workers use the new value.');
19989
20174
  refreshSettings();
19990
20175
  }
19991
20176
 
19992
20177
  async function setBudgetContextMode(mode) {
19993
- await postBudgetAction('/api/budgets/1m', { mode: mode });
20178
+ var d = await postBudgetAction('/api/budgets/1m', { mode: mode });
20179
+ if (d && d.ok) markRestartRequired('1M context changes need a Clementine restart before new Claude calls use the setting.');
19994
20180
  refreshSettings();
19995
20181
  }
19996
20182
 
@@ -20000,7 +20186,10 @@ async function forceBudgetOneMillion() {
20000
20186
  }
20001
20187
 
20002
20188
  async function applyBudgetDoctorFix() {
20003
- await postBudgetAction('/api/budgets/doctor-fix', {});
20189
+ var d = await postBudgetAction('/api/budgets/doctor-fix', {});
20190
+ if (d && d.ok && d.result && d.result.changed && d.result.changed.length) {
20191
+ markRestartRequired('Doctor Fix changed Clementine configuration. Restart Clementine to apply the fixes.');
20192
+ }
20004
20193
  refreshSettings();
20005
20194
  }
20006
20195
 
@@ -20104,8 +20293,7 @@ async function refreshSettings() {
20104
20293
  + '</div></div>';
20105
20294
 
20106
20295
  html += '<div style="padding:12px;color:var(--text-muted);font-size:12px">'
20107
- + '<strong>Note:</strong> Changes to API keys require a daemon restart to take effect. '
20108
- + 'Use <code>clementine restart</code> after updating channel tokens.'
20296
+ + '<strong>Note:</strong> Changes that require a daemon restart show a Restart Clementine prompt here in the dashboard.'
20109
20297
  + '</div>';
20110
20298
  container.innerHTML = html;
20111
20299
 
@@ -20168,8 +20356,11 @@ async function toggleSetting(el) {
20168
20356
  async function saveSettingValue(key, value) {
20169
20357
  var statusEl = document.getElementById('setting-' + key + '-status');
20170
20358
  try {
20171
- await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
20359
+ var result = await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
20172
20360
  if (statusEl) { statusEl.textContent = 'Saved'; statusEl.style.color = 'var(--green)'; setTimeout(function(){ statusEl.textContent = ''; }, 2000); }
20361
+ if (result && result.ok && settingRequiresDaemonRestart(key)) {
20362
+ markRestartRequired(key + ' changed. Restart Clementine so the daemon and channel workers use the new value.');
20363
+ }
20173
20364
  } catch(e) {
20174
20365
  if (statusEl) { statusEl.textContent = 'Error'; statusEl.style.color = 'var(--red)'; }
20175
20366
  }
@@ -20194,8 +20385,11 @@ async function saveAssistantPreferences() {
20194
20385
  async function removeSetting(key) {
20195
20386
  if (!confirm('Remove ' + key + ' from .env?')) return;
20196
20387
  try {
20197
- await apiDelete('/api/settings/' + encodeURIComponent(key));
20388
+ var result = await apiDelete('/api/settings/' + encodeURIComponent(key));
20198
20389
  toast(key + ' removed', 'success');
20390
+ if (result && result.ok && settingRequiresDaemonRestart(key)) {
20391
+ markRestartRequired(key + ' was removed. Restart Clementine so running workers stop using the old value.');
20392
+ }
20199
20393
  refreshSettings();
20200
20394
  } catch(e) { toast('Failed: ' + e, 'error'); }
20201
20395
  }
@@ -20225,8 +20419,11 @@ async function addCustomEnv() {
20225
20419
  if (!key || !value) { toast('Both key and value are required', 'error'); return; }
20226
20420
  if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) { toast('Invalid key format — use UPPER_SNAKE_CASE', 'error'); return; }
20227
20421
  try {
20228
- await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
20422
+ var result = await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
20229
20423
  toast(key + ' added', 'success');
20424
+ if (result && result.ok && settingRequiresDaemonRestart(key)) {
20425
+ markRestartRequired(key + ' was added. Restart Clementine so the daemon and channel workers can use it.');
20426
+ }
20230
20427
  keyInput.value = '';
20231
20428
  valInput.value = '';
20232
20429
  refreshSettings();
@@ -27596,6 +27793,7 @@ async function refreshSalesforce() {
27596
27793
 
27597
27794
  // ── Initial load — single batch request instead of 12+ parallel fetches ──
27598
27795
  (async function initDashboard() {
27796
+ renderRestartRequiredBanner();
27599
27797
  try {
27600
27798
  var r = await apiFetch('/api/init');
27601
27799
  var d = await r.json();
@@ -27667,6 +27865,7 @@ try {
27667
27865
  if (currentPage === 'home') refreshSessions();
27668
27866
  }
27669
27867
  if (evt.type === 'daemon_restarted') {
27868
+ clearRestartRequired();
27670
27869
  toast('Daemon restarted \u2014 refreshing data...', 'info');
27671
27870
  setTimeout(function() { refreshAll(); }, 1500);
27672
27871
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.15",
3
+ "version": "1.18.17",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",