clementine-agent 1.0.73 → 1.0.74

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.
@@ -21,8 +21,18 @@ export interface RecipeFieldPicker {
21
21
  /** Full tool name, e.g. "mcp__claude_ai_Google_Drive__search_files" */
22
22
  tool: string;
23
23
  /** Natural-language instruction to the probe agent — becomes the body of
24
- * "Call the tool X to {intent}, return a JSON array of {id, label}". */
24
+ * "Call the tool X to {intent}, return a JSON array of {id, label}".
25
+ * For typeahead pickers, may include the placeholder `{{query}}` which
26
+ * the server substitutes with the user's typed search string. */
25
27
  intent: string;
28
+ /** When set, the picker renders as a typeahead: user types at least
29
+ * `minQueryLength` chars (default 2), we debounce ~400ms, then fire the
30
+ * probe with `{{query}}` replaced. Use for tools whose list operation
31
+ * requires a query argument (e.g. search_contacts, Gmail search). */
32
+ queryArg?: string;
33
+ /** Minimum characters the user must type before firing the probe. Ignored
34
+ * when queryArg is unset. Default 2. */
35
+ minQueryLength?: number;
26
36
  /** If true, user can type a custom value instead of picking from the list
27
37
  * (falls back to the raw text). Useful when the source allows queries
28
38
  * that aren't enumerable. */
@@ -213,7 +213,9 @@ Steps:
213
213
  help: 'Pick an iMessage contact. Their phone number or email becomes the thread id.',
214
214
  picker: {
215
215
  tool: 'mcp__imessage__search_contacts',
216
- intent: 'list my iMessage contacts. Use the handle (phone number or email) as id, the display name (or handle if no name) as label, and the most-recent-message date as sublabel.',
216
+ intent: 'call search_contacts with query "{{query}}". For each match, output {id: the contact\'s phone number or email handle, label: the display name (or the handle if no name), sublabel: "handle: " + handle}. Return up to 15 results.',
217
+ queryArg: 'query',
218
+ minQueryLength: 2,
217
219
  allowCustom: true,
218
220
  },
219
221
  },
@@ -2635,15 +2635,20 @@ export async function cmdDashboard(opts) {
2635
2635
  try {
2636
2636
  const body = (req.body ?? {});
2637
2637
  const tool = String(body.tool ?? '').trim();
2638
- const intent = String(body.intent ?? '').trim();
2638
+ const rawIntent = String(body.intent ?? '').trim();
2639
+ const userQuery = String(body.query ?? '').trim();
2639
2640
  if (!tool) {
2640
2641
  res.status(400).json({ error: 'tool is required' });
2641
2642
  return;
2642
2643
  }
2643
- if (!intent) {
2644
+ if (!rawIntent) {
2644
2645
  res.status(400).json({ error: 'intent is required' });
2645
2646
  return;
2646
2647
  }
2648
+ // Typeahead pickers include a {{query}} placeholder. Substitute (and
2649
+ // escape any JSON-breakers) before cache key + prompt.
2650
+ const safeQuery = userQuery.replace(/"/g, '\\"').slice(0, 200);
2651
+ const intent = rawIntent.replace(/\{\{query\}\}/g, safeQuery);
2647
2652
  const cacheKey = `${tool}::${intent}`;
2648
2653
  if (!body.force) {
2649
2654
  const cached = brainProbeCache.get(cacheKey);
@@ -10784,12 +10789,43 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10784
10789
  brainFeedWizardRender();
10785
10790
  }
10786
10791
 
10792
+ // Per-field typeahead state: { [fieldKey]: { timer, lastQuery } }
10793
+ const brainPickerTypeahead = {};
10794
+
10787
10795
  async function brainRenderFieldPicker(field, values) {
10788
10796
  const container = document.querySelector('[data-field-picker="' + field.key + '"]');
10789
10797
  const hidden = document.querySelector('input[type=hidden][data-field="' + field.key + '"]');
10790
10798
  if (!container || !hidden) return;
10791
10799
  const picker = field.picker;
10792
10800
 
10801
+ // Typeahead mode: render a search box. User types → debounce → probe.
10802
+ if (picker.queryArg) {
10803
+ const minLen = picker.minQueryLength || 2;
10804
+ const currentVal = hidden.value || values[field.key] || '';
10805
+ container.innerHTML =
10806
+ '<input type="text" id="brain-picker-search-' + field.key + '" placeholder="Type to search (min ' + minLen + ' chars)…" style="width:100%" value="">' +
10807
+ '<div id="brain-picker-results-' + field.key + '" style="margin-top:6px;max-height:240px;overflow-y:auto"></div>' +
10808
+ (currentVal
10809
+ ? '<div style="font-size:12px;margin-top:6px">Selected: <code>' + escapeHtml(currentVal) + '</code></div>'
10810
+ : '');
10811
+ const searchEl = document.getElementById('brain-picker-search-' + field.key);
10812
+ searchEl.oninput = function() {
10813
+ const q = this.value.trim();
10814
+ if (brainPickerTypeahead[field.key] && brainPickerTypeahead[field.key].timer) {
10815
+ clearTimeout(brainPickerTypeahead[field.key].timer);
10816
+ }
10817
+ if (q.length < minLen) {
10818
+ document.getElementById('brain-picker-results-' + field.key).innerHTML =
10819
+ '<div style="color:var(--muted);font-size:12px;padding:6px">Type at least ' + minLen + ' characters…</div>';
10820
+ return;
10821
+ }
10822
+ brainPickerTypeahead[field.key] = {
10823
+ timer: setTimeout(function() { brainFireTypeaheadProbe(field, q); }, 400),
10824
+ };
10825
+ };
10826
+ return;
10827
+ }
10828
+
10793
10829
  try {
10794
10830
  const resp = await apiFetch('/api/brain/mcp/probe', {
10795
10831
  method: 'POST', headers: { 'content-type': 'application/json' },
@@ -10836,6 +10872,56 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10836
10872
  }
10837
10873
  }
10838
10874
 
10875
+ async function brainFireTypeaheadProbe(field, query) {
10876
+ const resultsEl = document.getElementById('brain-picker-results-' + field.key);
10877
+ const hidden = document.querySelector('input[type=hidden][data-field="' + field.key + '"]');
10878
+ if (!resultsEl || !hidden) return;
10879
+ resultsEl.innerHTML = '<div style="color:var(--muted);font-size:12px;padding:6px">Searching…</div>';
10880
+ try {
10881
+ const resp = await apiFetch('/api/brain/mcp/probe', {
10882
+ method: 'POST', headers: { 'content-type': 'application/json' },
10883
+ body: JSON.stringify({ tool: field.picker.tool, intent: field.picker.intent, query }),
10884
+ });
10885
+ const data = await resp.json();
10886
+ if (!resp.ok) throw new Error(data.error || 'probe failed');
10887
+ const items = data.items || [];
10888
+ if (!items.length) {
10889
+ resultsEl.innerHTML = '<div style="color:#8a5a00;font-size:12px;padding:6px">No matches for "' + escapeHtml(query) + '"' + (data.rawPreview ? ' (' + escapeHtml(data.rawPreview.slice(0, 100)) + ')' : '') + '</div>';
10890
+ return;
10891
+ }
10892
+ resultsEl.innerHTML = items.map(function(it) {
10893
+ const lbl = escapeHtml(it.label) + (it.sublabel ? ' <span style="color:var(--muted);font-size:11px">— ' + escapeHtml(it.sublabel) + '</span>' : '');
10894
+ return '<button class="btn" onclick="brainPickTypeaheadItem(\\'' + field.key + '\\', \\'' + encodeURIComponent(it.id) + '\\', \\'' + encodeURIComponent(it.label) + '\\')" style="display:block;width:100%;text-align:left;padding:6px 10px;margin-bottom:2px">' + lbl + '</button>';
10895
+ }).join('') +
10896
+ '<div style="font-size:11px;color:var(--muted);margin-top:4px">' + items.length + ' result' + (items.length === 1 ? '' : 's') + (data.cached ? ' (cached)' : '') + '</div>';
10897
+ } catch (err) {
10898
+ resultsEl.innerHTML = '<div style="color:#e66;font-size:12px;padding:6px">' + escapeHtml(String(err && err.message ? err.message : err)) + '</div>';
10899
+ }
10900
+ }
10901
+
10902
+ function brainPickTypeaheadItem(fieldKey, encodedId, encodedLabel) {
10903
+ const id = decodeURIComponent(encodedId);
10904
+ const label = decodeURIComponent(encodedLabel);
10905
+ const hidden = document.querySelector('input[type=hidden][data-field="' + fieldKey + '"]');
10906
+ if (hidden) hidden.value = id;
10907
+ // Show a confirmation chip and collapse the results.
10908
+ const container = document.querySelector('[data-field-picker="' + fieldKey + '"]');
10909
+ if (!container) return;
10910
+ const searchEl = document.getElementById('brain-picker-search-' + fieldKey);
10911
+ if (searchEl) searchEl.value = label;
10912
+ const resultsEl = document.getElementById('brain-picker-results-' + fieldKey);
10913
+ if (resultsEl) resultsEl.innerHTML = '';
10914
+ // Inject a "selected" line
10915
+ let sel = container.querySelector('.brain-picker-selected');
10916
+ if (!sel) {
10917
+ sel = document.createElement('div');
10918
+ sel.className = 'brain-picker-selected';
10919
+ sel.style.cssText = 'font-size:12px;margin-top:6px';
10920
+ container.appendChild(sel);
10921
+ }
10922
+ sel.innerHTML = '✓ Selected: <b>' + escapeHtml(label) + '</b> <code style="font-size:11px;color:var(--muted)">' + escapeHtml(id) + '</code>';
10923
+ }
10924
+
10839
10925
  function brainFieldPickerToggleCustom(fieldKey, encodedPicker) {
10840
10926
  const container = document.querySelector('[data-field-picker="' + fieldKey + '"]');
10841
10927
  const hidden = document.querySelector('input[type=hidden][data-field="' + fieldKey + '"]');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.73",
3
+ "version": "1.0.74",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",