clementine-agent 1.18.121 → 1.18.123

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.
@@ -14419,6 +14419,9 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14419
14419
  }
14420
14420
  .toast.success { border-left: 3px solid var(--green); }
14421
14421
  .toast.error { border-left: 3px solid var(--red); }
14422
+ /* 1.18.122 — variants used by callsites that lacked CSS support */
14423
+ .toast.warn { border-left: 3px solid var(--yellow); }
14424
+ .toast.info { border-left: 3px solid var(--accent); }
14422
14425
  @keyframes toastIn {
14423
14426
  from { transform: translateX(40px); opacity: 0; }
14424
14427
  to { transform: translateX(0); opacity: 1; }
@@ -19466,11 +19469,11 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19466
19469
  clearInterval(ticker);
19467
19470
 
19468
19471
  if (errorMsg) {
19469
- manifestEl.innerHTML = '<div style="color:#e66">Error: ' + escapeHtml(errorMsg) + '</div>';
19472
+ manifestEl.innerHTML = '<div style="color:var(--red)">Error: ' + escapeHtml(errorMsg) + '</div>';
19470
19473
  return;
19471
19474
  }
19472
19475
  if (!manifestData || !finalData) {
19473
- manifestEl.innerHTML = '<div style="color:#e66">Preview ended without a result. Check dashboard logs.</div>';
19476
+ manifestEl.innerHTML = '<div style="color:var(--red)">Preview ended without a result. Check dashboard logs.</div>';
19474
19477
  return;
19475
19478
  }
19476
19479
 
@@ -19480,13 +19483,13 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19480
19483
  const manifestRows = Object.entries(manifest.formats || {})
19481
19484
  .map(([fmt, n]) => '<tr><td>' + escapeHtml(fmt) + '</td><td>' + n + '</td></tr>').join('');
19482
19485
  const warnBlock = errorsList.length
19483
- ? '<div style="margin-top:10px;padding:10px;background:#fff3cd;border:1px solid #f0c36d;border-radius:6px;color:#8a5a00;font-size:13px">' +
19486
+ ? '<div style="margin-top:10px;padding:10px;background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.32);border-radius:6px;color:var(--text-primary);font-size:13px">' +
19484
19487
  '<div style="font-weight:600;margin-bottom:4px">' + errorsList.length + ' file(s) could not be ingested</div>' +
19485
19488
  errorsList.map((e) => '<div style="font-family:monospace;font-size:12px">• ' + escapeHtml(e.error) + '</div>').join('') +
19486
19489
  '</div>'
19487
19490
  : '';
19488
19491
  const emptyNote = (preview.length === 0 && !errorsList.length)
19489
- ? '<div style="margin-top:10px;padding:10px;background:#fff3cd;border:1px solid #f0c36d;border-radius:6px;color:#8a5a00;font-size:13px">No records extracted. The file may be empty or in an unsupported format.</div>'
19492
+ ? '<div style="margin-top:10px;padding:10px;background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.32);border-radius:6px;color:var(--text-primary);font-size:13px">No records extracted. The file may be empty or in an unsupported format.</div>'
19490
19493
  : '';
19491
19494
  manifestEl.innerHTML =
19492
19495
  '<div class="card" style="padding:12px"><div style="font-weight:600;margin-bottom:8px">Manifest</div>' +
@@ -19501,7 +19504,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19501
19504
  '<div style="font-weight:600">#' + (i + 1) + ' ' + escapeHtml(p.title || '(untitled)') + '</div>' +
19502
19505
  '<div style="color:var(--muted);font-size:12px;margin:4px 0">' + escapeHtml(p.targetRelPath || '') + '</div>' +
19503
19506
  '<div style="font-size:13px">' + escapeHtml((p.body || '').slice(0, 400)) + '</div>' +
19504
- (p.tags && p.tags.length ? '<div style="margin-top:6px;font-size:12px;color:#888">tags: ' + p.tags.map(escapeHtml).join(', ') + '</div>' : '') +
19507
+ (p.tags && p.tags.length ? '<div style="margin-top:6px;font-size:12px;color:var(--text-muted)">tags: ' + p.tags.map(escapeHtml).join(', ') + '</div>' : '') +
19505
19508
  '</div>').join('');
19506
19509
  previewEl.innerHTML =
19507
19510
  '<div style="font-weight:600;margin-bottom:8px">Preview (first ' + Math.min(preview.length, 10) + ' records, dry-run)</div>' + previewHtml;
@@ -19549,11 +19552,11 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19549
19552
  }
19550
19553
 
19551
19554
  if (errorMsg) {
19552
- progEl.innerHTML = '<div style="color:#e66">Error: ' + escapeHtml(errorMsg) + '</div>';
19555
+ progEl.innerHTML = '<div style="color:var(--red)">Error: ' + escapeHtml(errorMsg) + '</div>';
19553
19556
  return;
19554
19557
  }
19555
19558
  if (!finalData) {
19556
- progEl.innerHTML = '<div style="color:#e66">Ingestion ended without a result. Check dashboard logs.</div>';
19559
+ progEl.innerHTML = '<div style="color:var(--red)">Ingestion ended without a result. Check dashboard logs.</div>';
19557
19560
  return;
19558
19561
  }
19559
19562
  const elapsed = Math.floor((Date.now() - progress.startedAt) / 1000);
@@ -19564,7 +19567,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19564
19567
  ? 'Ingestion complete'
19565
19568
  : 'Ingestion finished, but nothing was written';
19566
19569
  const errBlock = errList.length
19567
- ? '<div style="margin-top:10px;padding:10px;background:#fff3cd;border:1px solid #f0c36d;border-radius:6px;color:#8a5a00;font-size:13px">' +
19570
+ ? '<div style="margin-top:10px;padding:10px;background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.32);border-radius:6px;color:var(--text-primary);font-size:13px">' +
19568
19571
  '<div style="font-weight:600;margin-bottom:4px">' + errList.length + ' error(s)</div>' +
19569
19572
  errList.map((e) => '<div style="font-family:monospace;font-size:12px">• ' + escapeHtml(e.error) + '</div>').join('') +
19570
19573
  '</div>'
@@ -19655,8 +19658,8 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19655
19658
  }
19656
19659
  el.innerHTML = data.integrations.map(function(i) {
19657
19660
  const ok = i.connected && i.hasFeedReadyTools;
19658
- const color = ok ? '#2f7d32' : '#8a5a00';
19659
- const bg = ok ? '#e8f5e9' : '#fff3cd';
19661
+ const color = ok ? 'var(--green)' : 'var(--text-primary)';
19662
+ const bg = ok ? 'rgba(34,197,94,0.10)' : 'rgba(245,158,11,0.10)';
19660
19663
  const dot = ok ? '✓' : '⚠';
19661
19664
  const source = i.kind === 'composio' ? 'Composio' : (i.kind === 'claude-desktop' ? 'Claude Desktop' : 'MCP');
19662
19665
  const label = ok ? i.label + ' · ' + source : i.label + ' (incomplete in ' + source + ')';
@@ -19685,7 +19688,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19685
19688
  : '<span style="color:var(--muted)">no fields</span>';
19686
19689
  return '<div class="card" style="padding:10px 12px;margin-bottom:8px;display:flex;align-items:center;gap:12px">' +
19687
19690
  '<div style="flex:1">' +
19688
- '<div style="font-weight:600">' + escapeHtml(f.name) + (f.enabled ? '' : ' <span style="color:#e66;font-weight:normal">(disabled)</span>') + '</div>' +
19691
+ '<div style="font-weight:600">' + escapeHtml(f.name) + (f.enabled ? '' : ' <span style="color:var(--red);font-weight:normal">(disabled)</span>') + '</div>' +
19689
19692
  '<div style="font-size:12px;color:var(--muted)">' +
19690
19693
  'Recipe: <code>' + escapeHtml(f.recipeId) + '</code> · Schedule: <code>' + escapeHtml(f.schedule) + '</code> · Target: <code>' + escapeHtml(f.targetFolder) + '</code>' +
19691
19694
  '</div>' +
@@ -19732,10 +19735,10 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19732
19735
  if (!brainFeedWizardState) return;
19733
19736
  const s = brainFeedWizardState;
19734
19737
  if (s.step === 0) {
19735
- if (!s.pick) { document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:#e66">Pick a connector.</span>'; return; }
19738
+ if (!s.pick) { document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:var(--red)">Pick a connector.</span>'; return; }
19736
19739
  s.step = 1;
19737
19740
  } else if (s.step === 1) {
19738
- if (!s.recipe) { document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:#e66">Pick a recipe.</span>'; return; }
19741
+ if (!s.recipe) { document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:var(--red)">Pick a recipe.</span>'; return; }
19739
19742
  s.values = {};
19740
19743
  for (const f of (s.recipe.fields || [])) {
19741
19744
  if (f.defaultValue) s.values[f.key] = f.defaultValue;
@@ -19752,11 +19755,11 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19752
19755
  const inputs = document.querySelectorAll('#brain-feed-wizard-step [data-field]');
19753
19756
  inputs.forEach(function(inp) { s.values[inp.dataset.field] = inp.value; });
19754
19757
  const missing = (s.recipe.fields || []).filter(function(f) { return f.required && !(s.values[f.key] || '').trim(); });
19755
- if (missing.length) { document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:#e66">Required: ' + missing.map(function(f) { return f.label; }).join(', ') + '</span>'; return; }
19758
+ if (missing.length) { document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:var(--red)">Required: ' + missing.map(function(f) { return f.label; }).join(', ') + '</span>'; return; }
19756
19759
  if (s.recipe && s.recipe.id === 'tool-backed-memory-seed') {
19757
19760
  const toolName = String(s.values.toolName || '').trim();
19758
19761
  if (!/^mcp__.+__.+$/.test(toolName)) {
19759
- document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:#e66">Pick an exact tool before continuing.</span>';
19762
+ document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:var(--red)">Pick an exact tool before continuing.</span>';
19760
19763
  return;
19761
19764
  }
19762
19765
  const rawVariables = String(s.values.variablesJson || '').trim();
@@ -19764,12 +19767,12 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19764
19767
  try {
19765
19768
  const parsedVariables = JSON.parse(rawVariables);
19766
19769
  if (!parsedVariables || typeof parsedVariables !== 'object' || Array.isArray(parsedVariables)) {
19767
- document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:#e66">Tool variables must be a JSON object, for example {}.</span>';
19770
+ document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:var(--red)">Tool variables must be a JSON object, for example {}.</span>';
19768
19771
  return;
19769
19772
  }
19770
19773
  } catch (err) {
19771
19774
  void err;
19772
- document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:#e66">Tool variables must be valid JSON, for example {}.</span>';
19775
+ document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:var(--red)">Tool variables must be valid JSON, for example {}.</span>';
19773
19776
  return;
19774
19777
  }
19775
19778
  }
@@ -19836,7 +19839,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19836
19839
  // can still set the value manually.
19837
19840
  container.innerHTML =
19838
19841
  '<input type="text" value="' + escapeHtml(currentVal) + '" placeholder="(type a value)" style="width:100%" oninput="(document.querySelector(\\'input[type=hidden][data-field=' + field.key + ']\\')||{}).value=this.value">' +
19839
- '<div style="color:#8a5a00;font-size:11px;margin-top:4px">Nothing returned from the probe — type a value manually' + (data.rawPreview ? ' (probe output: ' + escapeHtml(data.rawPreview.slice(0, 120)) + '…)' : '') + '</div>';
19842
+ '<div style="color:var(--text-primary);font-size:11px;margin-top:4px">Nothing returned from the probe — type a value manually' + (data.rawPreview ? ' (probe output: ' + escapeHtml(data.rawPreview.slice(0, 120)) + '…)' : '') + '</div>';
19840
19843
  return;
19841
19844
  }
19842
19845
 
@@ -19862,7 +19865,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19862
19865
  } catch (err) {
19863
19866
  container.innerHTML =
19864
19867
  '<input type="text" value="' + escapeHtml(values[field.key] || '') + '" placeholder="(probe failed — type manually)" style="width:100%" oninput="(document.querySelector(\\'input[type=hidden][data-field=' + field.key + ']\\')||{}).value=this.value">' +
19865
- '<div style="color:#e66;font-size:11px;margin-top:4px">Picker failed: ' + escapeHtml(String(err && err.message ? err.message : err)) + ' — type a value manually.</div>';
19868
+ '<div style="color:var(--red);font-size:11px;margin-top:4px">Picker failed: ' + escapeHtml(String(err && err.message ? err.message : err)) + ' — type a value manually.</div>';
19866
19869
  }
19867
19870
  }
19868
19871
 
@@ -19888,7 +19891,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19888
19891
  const rawHint = trimmed && !isEmptyArray
19889
19892
  ? ' <span style="color:var(--muted)">Tool said: ' + escapeHtml(trimmed.slice(0, 140)) + '</span>'
19890
19893
  : '';
19891
- resultsEl.innerHTML = '<div style="color:#8a5a00;font-size:12px;padding:6px">No matches for "' + escapeHtml(query) + '". The tool may be limited by macOS permissions or not support this query — use <a href="#" onclick="brainFieldPickerToggleCustom(\\'' + field.key + '\\', \\'\\');return false">type a value directly</a>.' + rawHint + '</div>';
19894
+ resultsEl.innerHTML = '<div style="color:var(--text-primary);font-size:12px;padding:6px">No matches for "' + escapeHtml(query) + '". The tool may be limited by macOS permissions or not support this query — use <a href="#" onclick="brainFieldPickerToggleCustom(\\'' + field.key + '\\', \\'\\');return false">type a value directly</a>.' + rawHint + '</div>';
19892
19895
  return;
19893
19896
  }
19894
19897
  resultsEl.innerHTML = items.map(function(it) {
@@ -19897,7 +19900,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19897
19900
  }).join('') +
19898
19901
  '<div style="font-size:11px;color:var(--muted);margin-top:4px">' + items.length + ' result' + (items.length === 1 ? '' : 's') + (data.cached ? ' (cached)' : '') + '</div>';
19899
19902
  } catch (err) {
19900
- resultsEl.innerHTML = '<div style="color:#e66;font-size:12px;padding:6px">' + escapeHtml(String(err && err.message ? err.message : err)) + '</div>';
19903
+ resultsEl.innerHTML = '<div style="color:var(--red);font-size:12px;padding:6px">' + escapeHtml(String(err && err.message ? err.message : err)) + '</div>';
19901
19904
  }
19902
19905
  }
19903
19906
 
@@ -19963,7 +19966,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
19963
19966
  let html = '';
19964
19967
  if (s.step === 0) {
19965
19968
  if (!s.connected.length) {
19966
- html = '<div style="color:#8a5a00">No connectors have feed-ready tools. Connect Composio or open Claude Desktop → Connectors and sign into Google Drive, Outlook, Gmail, or Slack first.</div>';
19969
+ html = '<div style="color:var(--text-primary)">No connectors have feed-ready tools. Connect Composio or open Claude Desktop → Connectors and sign into Google Drive, Outlook, Gmail, or Slack first.</div>';
19967
19970
  } else {
19968
19971
  html = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px">' +
19969
19972
  s.connected.map(function(i) {
@@ -20026,7 +20029,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20026
20029
  } else {
20027
20030
  control = '<input type="text" data-field="' + f.key + '" value="' + escapeHtml(val) + '" placeholder="' + escapeHtml(f.placeholder || '') + '" style="width:100%">';
20028
20031
  }
20029
- return '<label style="font-weight:500;padding-top:6px">' + escapeHtml(f.label) + (f.required ? ' <span style="color:#e66">*</span>' : '') + '</label>' +
20032
+ return '<label style="font-weight:500;padding-top:6px">' + escapeHtml(f.label) + (f.required ? ' <span style="color:var(--red)">*</span>' : '') + '</label>' +
20030
20033
  '<div>' + control + help + '</div>';
20031
20034
  }).join('') + '</div>';
20032
20035
 
@@ -20091,13 +20094,13 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20091
20094
  });
20092
20095
  const data = await resp.json();
20093
20096
  if (!resp.ok) {
20094
- document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:#e66">' + escapeHtml(data.error || 'save failed') + '</span>';
20097
+ document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:var(--red)">' + escapeHtml(data.error || 'save failed') + '</span>';
20095
20098
  return;
20096
20099
  }
20097
20100
  brainCloseFeedWizard();
20098
20101
  brainLoadFeeds();
20099
20102
  } catch (err) {
20100
- document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:#e66">' + escapeHtml(String(err)) + '</span>';
20103
+ document.getElementById('brain-feed-wizard-status').innerHTML = '<span style="color:var(--red)">' + escapeHtml(String(err)) + '</span>';
20101
20104
  }
20102
20105
  }
20103
20106
 
@@ -20210,8 +20213,8 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20210
20213
  const slug = document.getElementById('brain-webhook-slug').value.trim();
20211
20214
  const folder = document.getElementById('brain-webhook-folder').value.trim() || ('04-Ingest/' + slug);
20212
20215
  const statusEl = document.getElementById('brain-webhook-status');
20213
- if (!slug) { statusEl.innerHTML = '<span style="color:#e66">slug required</span>'; return; }
20214
- if (!/^[a-z][a-z0-9_-]*$/.test(slug)) { statusEl.innerHTML = '<span style="color:#e66">slug must be lowercase alphanumeric</span>'; return; }
20216
+ if (!slug) { statusEl.innerHTML = '<span style="color:var(--red)">slug required</span>'; return; }
20217
+ if (!/^[a-z][a-z0-9_-]*$/.test(slug)) { statusEl.innerHTML = '<span style="color:var(--red)">slug must be lowercase alphanumeric</span>'; return; }
20215
20218
 
20216
20219
  // 1) Generate a random 32-byte secret and save it under ref "webhook_<slug>"
20217
20220
  const bytes = new Uint8Array(32);
@@ -20226,7 +20229,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20226
20229
  });
20227
20230
  if (!credResp.ok) {
20228
20231
  const e = await credResp.json();
20229
- statusEl.innerHTML = '<span style="color:#e66">Secret save failed: ' + escapeHtml(e.error || 'unknown') + '</span>';
20232
+ statusEl.innerHTML = '<span style="color:var(--red)">Secret save failed: ' + escapeHtml(e.error || 'unknown') + '</span>';
20230
20233
  return;
20231
20234
  }
20232
20235
 
@@ -20246,7 +20249,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20246
20249
  });
20247
20250
  if (!resp.ok) {
20248
20251
  const e = await resp.json();
20249
- statusEl.innerHTML = '<span style="color:#e66">' + escapeHtml(e.error || 'save failed') + '</span>';
20252
+ statusEl.innerHTML = '<span style="color:var(--red)">' + escapeHtml(e.error || 'save failed') + '</span>';
20250
20253
  return;
20251
20254
  }
20252
20255
 
@@ -20271,7 +20274,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20271
20274
  const cronExpr = document.getElementById('brain-poll-cron').value.trim();
20272
20275
  const folder = document.getElementById('brain-poll-folder').value.trim();
20273
20276
  const statusEl = document.getElementById('brain-poll-status');
20274
- if (!slug || !url) { statusEl.innerHTML = '<span style="color:#e66">slug and URL required</span>'; return; }
20277
+ if (!slug || !url) { statusEl.innerHTML = '<span style="color:var(--red)">slug and URL required</span>'; return; }
20275
20278
 
20276
20279
  const headers = brainCollectKv('headers');
20277
20280
  const params = brainCollectKv('params');
@@ -20293,7 +20296,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20293
20296
  }),
20294
20297
  });
20295
20298
  const data = await resp.json();
20296
- if (!resp.ok) { statusEl.innerHTML = '<span style="color:#e66">' + escapeHtml(data.error || 'save failed') + '</span>'; return; }
20299
+ if (!resp.ok) { statusEl.innerHTML = '<span style="color:var(--red)">' + escapeHtml(data.error || 'save failed') + '</span>'; return; }
20297
20300
  statusEl.innerHTML = '<span style="color:#4ade80">✓ Saved</span>';
20298
20301
 
20299
20302
  if (runNow) {
@@ -20302,7 +20305,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20302
20305
  const runData = await runResp.json();
20303
20306
  statusEl.innerHTML = runResp.ok
20304
20307
  ? '<span style="color:#4ade80">✓ Saved + run: in=' + runData.recordsIn + ' written=' + runData.recordsWritten + '</span>'
20305
- : '<span style="color:#e66">Saved but run failed: ' + escapeHtml(runData.error || 'unknown') + '</span>';
20308
+ : '<span style="color:var(--red)">Saved but run failed: ' + escapeHtml(runData.error || 'unknown') + '</span>';
20306
20309
  }
20307
20310
  brainLoadSources();
20308
20311
  }
@@ -20323,7 +20326,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20323
20326
  const ref = document.getElementById('brain-cred-ref').value.trim();
20324
20327
  const value = document.getElementById('brain-cred-val').value;
20325
20328
  const statusEl = document.getElementById('brain-cred-status');
20326
- if (!ref || !value) { statusEl.innerHTML = '<span style="color:#e66">ref and value required</span>'; return; }
20329
+ if (!ref || !value) { statusEl.innerHTML = '<span style="color:var(--red)">ref and value required</span>'; return; }
20327
20330
  const resp = await apiFetch('/api/brain/credentials', {
20328
20331
  method: 'POST', headers: { 'content-type': 'application/json' },
20329
20332
  body: JSON.stringify({ ref, value }),
@@ -20331,7 +20334,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20331
20334
  const data = await resp.json();
20332
20335
  statusEl.innerHTML = resp.ok
20333
20336
  ? '<span style="color:#4ade80">✓ Saved ' + escapeHtml(ref) + '</span>'
20334
- : '<span style="color:#e66">' + escapeHtml(data.error || 'save failed') + '</span>';
20337
+ : '<span style="color:var(--red)">' + escapeHtml(data.error || 'save failed') + '</span>';
20335
20338
  document.getElementById('brain-cred-ref').value = '';
20336
20339
  document.getElementById('brain-cred-val').value = '';
20337
20340
  brainShowCredsForm(); // refresh list
@@ -20425,7 +20428,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20425
20428
  });
20426
20429
  const data = await resp.json();
20427
20430
  if (!resp.ok) {
20428
- statusEl.innerHTML = '<span style="color:#e66">Upload failed: ' + escapeHtml(data.error || 'unknown') + '</span>';
20431
+ statusEl.innerHTML = '<span style="color:var(--red)">Upload failed: ' + escapeHtml(data.error || 'unknown') + '</span>';
20429
20432
  return;
20430
20433
  }
20431
20434
  statusEl.innerHTML = '<span style="color:#4ade80">✓ Uploaded ' + data.count + ' file(s)</span>';
@@ -20433,7 +20436,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
20433
20436
  // Kick off preview immediately
20434
20437
  await brainPreviewSeed();
20435
20438
  } catch (err) {
20436
- statusEl.innerHTML = '<span style="color:#e66">Upload error: ' + escapeHtml(String(err)) + '</span>';
20439
+ statusEl.innerHTML = '<span style="color:var(--red)">Upload error: ' + escapeHtml(String(err)) + '</span>';
20437
20440
  }
20438
20441
  }
20439
20442
 
@@ -79,6 +79,87 @@ function normalizeStringArray(v) {
79
79
  const out = Array.from(new Set(v.map(x => typeof x === 'string' ? x.trim() : '').filter(Boolean)));
80
80
  return out.length > 0 ? out : undefined;
81
81
  }
82
+ /**
83
+ * Single-source-of-truth YAML → CronJobDefinition parser.
84
+ *
85
+ * Used by parseCronJobs (global CRON.md) AND parseAgentCronJobs (per-agent).
86
+ * Caller is responsible for handling the name prefix (agent jobs are
87
+ * prefixed with `<slug>:`) and for tagging agentSlug.
88
+ *
89
+ * Returns null when the row is malformed (missing name/schedule/prompt) —
90
+ * the caller logs + skips. Accepts both snake_case and camelCase YAML keys
91
+ * since users hand-edit CRON.md and we want to be forgiving.
92
+ *
93
+ * Centralizing here closes the drift the previous audit flagged: the agent
94
+ * variant was missing alwaysDeliver/attachments/requiresConfirmation/
95
+ * confirmationTimeoutMin and the description field. After this refactor
96
+ * both surfaces have the same field set.
97
+ */
98
+ function parseJobYaml(job) {
99
+ const name = String(job.name ?? '');
100
+ const schedule = String(job.schedule ?? '');
101
+ const prompt = String(job.prompt ?? '');
102
+ if (!name || !schedule || !prompt)
103
+ return null;
104
+ const enabled = job.enabled !== false;
105
+ const tier = Number(job.tier ?? 1);
106
+ const maxTurns = job.max_turns != null ? Number(job.max_turns) : undefined;
107
+ const model = job.model != null ? String(job.model) : undefined;
108
+ const workDir = job.work_dir != null ? String(job.work_dir) : undefined;
109
+ const mode = job.mode === 'unleashed' ? 'unleashed' : 'standard';
110
+ const maxHours = job.max_hours != null ? Number(job.max_hours) : undefined;
111
+ const maxRetries = job.max_retries != null ? Number(job.max_retries) : undefined;
112
+ const after = job.after != null ? String(job.after) : undefined;
113
+ const successCriteria = Array.isArray(job.success_criteria)
114
+ ? job.success_criteria.map(c => String(c))
115
+ : undefined;
116
+ // Prefer free-form successCriteriaText; fall back to joining the legacy
117
+ // string[] so legacy YAML keeps rendering. Writes go to the new field.
118
+ let successCriteriaText = typeof job.success_criteria_text === 'string'
119
+ ? String(job.success_criteria_text)
120
+ : (typeof job.successCriteriaText === 'string' ? String(job.successCriteriaText) : undefined);
121
+ if (!successCriteriaText && Array.isArray(successCriteria) && successCriteria.length > 0) {
122
+ successCriteriaText = successCriteria.join('\n');
123
+ }
124
+ const successSchemaRaw = job.success_schema ?? job.successSchema;
125
+ const successSchema = (successSchemaRaw && typeof successSchemaRaw === 'object' && !Array.isArray(successSchemaRaw))
126
+ ? successSchemaRaw
127
+ : undefined;
128
+ const addDirs = normalizeStringArray(job.add_dirs ?? job.addDirs);
129
+ const alwaysDeliver = job.always_deliver === true ? true : undefined;
130
+ const context = job.context != null ? String(job.context) : undefined;
131
+ const preCheck = job.pre_check != null ? String(job.pre_check) : undefined;
132
+ const attachments = normalizeStringArray(job.attachments);
133
+ const requiresConfirmation = job.requires_confirmation === true || job.requiresConfirmation === true ? true : undefined;
134
+ const confirmationTimeoutMin = job.confirmation_timeout_min != null
135
+ ? Number(job.confirmation_timeout_min)
136
+ : (job.confirmationTimeoutMin != null ? Number(job.confirmationTimeoutMin) : undefined);
137
+ // Per-job agent scoping (a global cron can be scoped to a specific
138
+ // agent's profile). Accept both casings.
139
+ const agentSlugRaw = job.agentSlug ?? job.agent_slug;
140
+ const agentSlug = typeof agentSlugRaw === 'string' && /^[a-z0-9-]+$/i.test(agentSlugRaw)
141
+ ? agentSlugRaw
142
+ : undefined;
143
+ const skills = normalizeStringArray(job.skills);
144
+ const allowedTools = normalizeStringArray(job.allowed_tools ?? job.allowedTools);
145
+ const allowedMcpServers = normalizeStringArray(job.allowed_mcp_servers ?? job.allowedMcpServers);
146
+ const tags = normalizeStringArray(job.tags);
147
+ const categoryRaw = job.category;
148
+ const category = typeof categoryRaw === 'string' && categoryRaw.trim()
149
+ ? categoryRaw.trim().slice(0, 64)
150
+ : undefined;
151
+ const predictable = typeof job.predictable === 'boolean' ? job.predictable : undefined;
152
+ const description = typeof job.description === 'string' && job.description.trim()
153
+ ? job.description.trim().slice(0, 500)
154
+ : undefined;
155
+ return {
156
+ name, schedule, prompt, enabled, tier, description, maxTurns, model, workDir, mode,
157
+ maxHours, maxRetries, after, successCriteria, successCriteriaText, successSchema, addDirs,
158
+ alwaysDeliver, context, preCheck, attachments, requiresConfirmation, confirmationTimeoutMin,
159
+ agentSlug,
160
+ skills, allowedTools, allowedMcpServers, tags, category, predictable,
161
+ };
162
+ }
82
163
  /**
83
164
  * Parse cron job definitions from vault/00-System/CRON.md frontmatter.
84
165
  * Used by both the in-process CronScheduler and the standalone CLI runner.
@@ -86,10 +167,9 @@ function normalizeStringArray(v) {
86
167
  export function parseCronJobs() {
87
168
  if (!existsSync(CRON_FILE))
88
169
  return [];
89
- const raw = readFileSync(CRON_FILE, 'utf-8');
90
170
  let parsed;
91
171
  try {
92
- parsed = matter(raw);
172
+ parsed = matter(readFileSync(CRON_FILE, 'utf-8'));
93
173
  }
94
174
  catch (err) {
95
175
  logger.error({ err }, 'CRON.md YAML parse error — keeping previous jobs. Fix the file manually.');
@@ -98,76 +178,11 @@ export function parseCronJobs() {
98
178
  const jobDefs = (parsed.data.jobs ?? []);
99
179
  const jobs = [];
100
180
  for (const job of jobDefs) {
101
- const name = String(job.name ?? '');
102
- const schedule = String(job.schedule ?? '');
103
- const prompt = String(job.prompt ?? '');
104
- const enabled = job.enabled !== false;
105
- const tier = Number(job.tier ?? 1);
106
- const maxTurns = job.max_turns != null ? Number(job.max_turns) : undefined;
107
- const model = job.model != null ? String(job.model) : undefined;
108
- const workDir = job.work_dir != null ? String(job.work_dir) : undefined;
109
- const mode = job.mode === 'unleashed' ? 'unleashed' : 'standard';
110
- const maxHours = job.max_hours != null ? Number(job.max_hours) : undefined;
111
- const maxRetries = job.max_retries != null ? Number(job.max_retries) : undefined;
112
- const after = job.after != null ? String(job.after) : undefined;
113
- const successCriteria = Array.isArray(job.success_criteria)
114
- ? job.success_criteria.map(c => String(c))
115
- : undefined;
116
- // PRD Phase 1: prefer success_criteria_text (free-form). On read, fall
117
- // back to joining the legacy success_criteria string[] so legacy YAML
118
- // keeps rendering in the new editor surface. Writes go to the new field.
119
- let successCriteriaText = typeof job.success_criteria_text === 'string'
120
- ? String(job.success_criteria_text)
121
- : (typeof job.successCriteriaText === 'string' ? String(job.successCriteriaText) : undefined);
122
- if (!successCriteriaText && Array.isArray(successCriteria) && successCriteria.length > 0) {
123
- successCriteriaText = successCriteria.join('\n');
124
- }
125
- // PRD Phase 1: JSON Schema validated against ResultMessage.structured_output.
126
- // Accept either snake_case (success_schema) or camelCase from API. Stored
127
- // as a plain object; ajv is loaded lazily at validation time.
128
- const successSchemaRaw = job.success_schema ?? job.successSchema;
129
- const successSchema = (successSchemaRaw && typeof successSchemaRaw === 'object' && !Array.isArray(successSchemaRaw))
130
- ? successSchemaRaw
131
- : undefined;
132
- // PRD Phase 1: read scope beyond cwd. Accept either casing.
133
- const addDirs = normalizeStringArray(job.add_dirs ?? job.addDirs);
134
- const alwaysDeliver = job.always_deliver === true ? true : undefined;
135
- const context = job.context != null ? String(job.context) : undefined;
136
- const preCheck = job.pre_check != null ? String(job.pre_check) : undefined;
137
- // Optional: scope a global job to a specific agent's profile (loads
138
- // the agent's allowedTools whitelist, system prompt, etc.). Accept
139
- // both camelCase and snake_case to be forgiving of user-written YAML.
140
- const agentSlugRaw = job.agentSlug ?? job.agent_slug;
141
- const agentSlug = typeof agentSlugRaw === 'string' && /^[a-z0-9-]+$/i.test(agentSlugRaw)
142
- ? agentSlugRaw
143
- : undefined;
144
- // ── Trick capabilities — accept both camelCase and snake_case ─────
145
- const skills = normalizeStringArray(job.skills);
146
- const allowedTools = normalizeStringArray(job.allowed_tools ?? job.allowedTools);
147
- const allowedMcpServers = normalizeStringArray(job.allowed_mcp_servers ?? job.allowedMcpServers);
148
- const tags = normalizeStringArray(job.tags);
149
- const categoryRaw = job.category;
150
- const category = typeof categoryRaw === 'string' && categoryRaw.trim()
151
- ? categoryRaw.trim().slice(0, 64)
152
- : undefined;
153
- // Predictable (contract) mode — undefined means legacy behavior.
154
- const predictable = typeof job.predictable === 'boolean' ? job.predictable : undefined;
155
- // 1.18.119 — human-readable description (used by the task card preview
156
- // and by the cron-clean migrator to surface what each job does without
157
- // showing raw prompt boilerplate).
158
- const description = typeof job.description === 'string' && job.description.trim()
159
- ? job.description.trim().slice(0, 500)
160
- : undefined;
161
- if (!name || !schedule || !prompt) {
181
+ const def = parseJobYaml(job);
182
+ if (def)
183
+ jobs.push(def);
184
+ else
162
185
  logger.warn({ job }, 'Skipping malformed cron job');
163
- continue;
164
- }
165
- jobs.push({
166
- name, schedule, prompt, enabled, tier, description, maxTurns, model, workDir, mode,
167
- maxHours, maxRetries, after, successCriteria, successCriteriaText, successSchema, addDirs,
168
- alwaysDeliver, context, preCheck, agentSlug,
169
- skills, allowedTools, allowedMcpServers, tags, category, predictable,
170
- });
171
186
  }
172
187
  return jobs;
173
188
  }
@@ -193,72 +208,20 @@ export function parseAgentCronJobs(agentsDir) {
193
208
  if (!existsSync(cronFile))
194
209
  continue;
195
210
  try {
196
- const raw = readFileSync(cronFile, 'utf-8');
197
- const parsed = matter(raw);
211
+ const parsed = matter(readFileSync(cronFile, 'utf-8'));
198
212
  const jobDefs = (parsed.data.jobs ?? []);
199
213
  for (const job of jobDefs) {
200
- const name = String(job.name ?? '');
201
- const schedule = String(job.schedule ?? '');
202
- const prompt = String(job.prompt ?? '');
203
- const enabled = job.enabled !== false;
204
- const tier = Number(job.tier ?? 1);
205
- const maxTurns = job.max_turns != null ? Number(job.max_turns) : undefined;
206
- const model = job.model != null ? String(job.model) : undefined;
207
- const workDir = job.work_dir != null ? String(job.work_dir) : undefined;
208
- const mode = job.mode === 'unleashed' ? 'unleashed' : 'standard';
209
- const maxHours = job.max_hours != null ? Number(job.max_hours) : undefined;
210
- const maxRetries = job.max_retries != null ? Number(job.max_retries) : undefined;
211
- const after = job.after != null ? String(job.after) : undefined;
212
- const successCriteria = Array.isArray(job.success_criteria)
213
- ? job.success_criteria.map(c => String(c))
214
- : undefined;
215
- // PRD Phase 1 fields — symmetric with global parser above.
216
- let successCriteriaText = typeof job.success_criteria_text === 'string'
217
- ? String(job.success_criteria_text)
218
- : (typeof job.successCriteriaText === 'string' ? String(job.successCriteriaText) : undefined);
219
- if (!successCriteriaText && Array.isArray(successCriteria) && successCriteria.length > 0) {
220
- successCriteriaText = successCriteria.join('\n');
221
- }
222
- const successSchemaRaw = job.success_schema ?? job.successSchema;
223
- const successSchema = (successSchemaRaw && typeof successSchemaRaw === 'object' && !Array.isArray(successSchemaRaw))
224
- ? successSchemaRaw
225
- : undefined;
226
- const addDirs = normalizeStringArray(job.add_dirs ?? job.addDirs);
227
- const context = job.context != null ? String(job.context) : undefined;
228
- const preCheck = job.pre_check != null ? String(job.pre_check) : undefined;
229
- // ── Trick capabilities — symmetric with global parser ─────────
230
- // (NB: this parser still lacks alwaysDeliver/attachments/
231
- // requiresConfirmation/confirmationTimeoutMin from the global
232
- // parser — pre-existing drift, fix in a separate change.)
233
- const skills = normalizeStringArray(job.skills);
234
- const allowedTools = normalizeStringArray(job.allowed_tools ?? job.allowedTools);
235
- const allowedMcpServers = normalizeStringArray(job.allowed_mcp_servers ?? job.allowedMcpServers);
236
- const tags = normalizeStringArray(job.tags);
237
- const categoryRaw = job.category;
238
- const category = typeof categoryRaw === 'string' && categoryRaw.trim()
239
- ? categoryRaw.trim().slice(0, 64)
240
- : undefined;
241
- const predictable = typeof job.predictable === 'boolean' ? job.predictable : undefined;
242
- // 1.18.119 — symmetric with the global parseCronJobs description
243
- // field. Without this, agent jobs always look "missing description"
244
- // to the cron-clean migrator and stay flagged as eligible forever.
245
- const description = typeof job.description === 'string' && job.description.trim()
246
- ? job.description.trim().slice(0, 500)
247
- : undefined;
248
- if (!name || !schedule || !prompt) {
214
+ const def = parseJobYaml(job);
215
+ if (!def) {
249
216
  logger.warn({ job, agent: slug }, 'Skipping malformed agent cron job');
250
217
  continue;
251
218
  }
252
- // Prefix name with agent slug and tag with agentSlug
253
- allJobs.push({
254
- name: `${slug}:${name}`,
255
- schedule, prompt, enabled, tier, description, maxTurns, model, workDir,
256
- mode, maxHours, maxRetries, after,
257
- successCriteria, successCriteriaText, successSchema, addDirs,
258
- context, preCheck,
259
- agentSlug: slug,
260
- skills, allowedTools, allowedMcpServers, tags, category, predictable,
261
- });
219
+ // Agent CRON.md stores BARE job names; we prefix with the slug at
220
+ // read time so the runtime can route by `<slug>:<job>` and the
221
+ // dashboard can disambiguate same-named jobs across agents.
222
+ // agentSlug is stamped from the folder location, overriding any
223
+ // value in the YAML — single source of truth.
224
+ allJobs.push({ ...def, name: `${slug}:${def.name}`, agentSlug: slug });
262
225
  }
263
226
  }
264
227
  catch (err) {
package/dist/types.d.ts CHANGED
@@ -767,23 +767,21 @@ export interface Feedback {
767
767
  comment?: string;
768
768
  createdAt?: string;
769
769
  }
770
- export interface BehavioralCorrection {
771
- correction: string;
772
- category: 'verbosity' | 'tone' | 'workflow' | 'format' | 'accuracy' | 'proactivity' | 'scope';
773
- strength: 'explicit' | 'implicit';
774
- }
775
- export interface PreferenceLearned {
776
- preference: string;
777
- confidence: 'high' | 'medium' | 'low';
778
- }
779
770
  export interface SessionReflection {
780
771
  id?: number;
781
772
  sessionKey: string;
782
773
  exchangeCount: number;
783
774
  frictionSignals: string[];
784
775
  qualityScore: number;
785
- behavioralCorrections: BehavioralCorrection[];
786
- preferencesLearned: PreferenceLearned[];
776
+ behavioralCorrections: Array<{
777
+ correction: string;
778
+ category: 'verbosity' | 'tone' | 'workflow' | 'format' | 'accuracy' | 'proactivity' | 'scope';
779
+ strength: 'explicit' | 'implicit';
780
+ }>;
781
+ preferencesLearned: Array<{
782
+ preference: string;
783
+ confidence: 'high' | 'medium' | 'low';
784
+ }>;
787
785
  agentSlug?: string;
788
786
  createdAt?: string;
789
787
  }
@@ -835,13 +833,6 @@ export interface ExecutionPlan {
835
833
  steps: PlanStep[];
836
834
  synthesisPrompt: string;
837
835
  }
838
- export interface PlanProgressUpdate {
839
- stepId: string;
840
- status: 'waiting' | 'running' | 'done' | 'failed';
841
- description: string;
842
- durationMs?: number;
843
- resultPreview?: string;
844
- }
845
836
  export interface WorkflowInput {
846
837
  type: 'string' | 'number';
847
838
  default?: string;
@@ -853,13 +844,6 @@ export interface WorkflowStepMcpConfig {
853
844
  tool: string;
854
845
  inputs?: Record<string, unknown>;
855
846
  }
856
- export interface WorkflowStepCliConfig {
857
- cmd: string;
858
- args?: string[];
859
- workDir?: string;
860
- timeoutMs?: number;
861
- captureStderr?: boolean;
862
- }
863
847
  export interface WorkflowStepChannelConfig {
864
848
  channel: 'discord' | 'slack' | 'telegram' | 'whatsapp' | 'email' | 'webhook';
865
849
  target: string;
@@ -891,7 +875,16 @@ export interface WorkflowStep {
891
875
  workDir?: string;
892
876
  kind?: WorkflowStepKind;
893
877
  mcp?: WorkflowStepMcpConfig;
894
- cli?: WorkflowStepCliConfig;
878
+ /** CLI step config — inline shape (was the standalone WorkflowStepCliConfig
879
+ * type that had zero external references; the field stays but the
880
+ * named alias was dropped in 1.18.122). */
881
+ cli?: {
882
+ cmd: string;
883
+ args?: string[];
884
+ workDir?: string;
885
+ timeoutMs?: number;
886
+ captureStderr?: boolean;
887
+ };
895
888
  channel?: WorkflowStepChannelConfig;
896
889
  transform?: WorkflowStepTransformConfig;
897
890
  conditional?: WorkflowStepConditionalConfig;
@@ -1180,14 +1173,6 @@ export interface SessionRecord {
1180
1173
  lastUsedAt: number;
1181
1174
  userAgent?: string;
1182
1175
  }
1183
- export interface ConfigRevision {
1184
- id?: number;
1185
- agentSlug: string;
1186
- fileName: string;
1187
- content: string;
1188
- changedBy?: string;
1189
- createdAt?: string;
1190
- }
1191
1176
  export interface Lead {
1192
1177
  id?: number;
1193
1178
  agentSlug: string;
@@ -1202,16 +1187,6 @@ export interface Lead {
1202
1187
  createdAt?: string;
1203
1188
  updatedAt?: string;
1204
1189
  }
1205
- export interface SequenceEnrollment {
1206
- id?: number;
1207
- leadId: number;
1208
- sequenceName: string;
1209
- currentStep: number;
1210
- status: 'active' | 'paused' | 'replied' | 'completed' | 'opted_out';
1211
- nextStepDueAt?: string;
1212
- startedAt?: string;
1213
- updatedAt?: string;
1214
- }
1215
1190
  export interface Activity {
1216
1191
  id?: number;
1217
1192
  leadId?: number;
@@ -1222,40 +1197,6 @@ export interface Activity {
1222
1197
  templateUsed?: string;
1223
1198
  performedAt?: string;
1224
1199
  }
1225
- export interface SuppressionEntry {
1226
- id?: number;
1227
- email: string;
1228
- reason: 'unsubscribe' | 'bounce' | 'manual' | 'complaint';
1229
- addedAt?: string;
1230
- addedBy?: string;
1231
- }
1232
- export interface ApprovalRequest {
1233
- id?: number;
1234
- agentSlug: string;
1235
- actionType: 'email_send' | 'sequence_start' | 'escalation';
1236
- summary: string;
1237
- detail?: Record<string, unknown>;
1238
- status: 'pending' | 'approved' | 'rejected';
1239
- requestedAt?: string;
1240
- resolvedAt?: string;
1241
- resolvedBy?: string;
1242
- }
1243
- export interface SfSyncRecord {
1244
- id?: number;
1245
- localTable: 'leads' | 'activities';
1246
- localId: number;
1247
- sfObjectType: 'Lead' | 'Contact' | 'Opportunity' | 'Task' | 'Event';
1248
- sfId: string;
1249
- syncDirection: 'push' | 'pull';
1250
- syncedAt?: string;
1251
- syncStatus: 'success' | 'error' | 'conflict';
1252
- errorMessage?: string;
1253
- }
1254
- export interface SfFieldMapping {
1255
- localField: string;
1256
- sfField: string;
1257
- direction: 'bidirectional' | 'push-only' | 'pull-only';
1258
- }
1259
1200
  /** Supported v1 ingest formats detected by format-detector. */
1260
1201
  export type DetectedFormat = 'csv' | 'json' | 'jsonl' | 'markdown' | 'pdf' | 'email' | 'docx' | 'unknown';
1261
1202
  /** Operational mode for a source. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.121",
3
+ "version": "1.18.123",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",