clementine-agent 1.0.73 → 1.0.75

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. */
@@ -208,14 +208,10 @@ Steps:
208
208
  fields: [
209
209
  {
210
210
  key: 'contact',
211
- label: 'Contact',
211
+ label: 'Phone number or email',
212
+ placeholder: '+15551234567 or someone@example.com',
212
213
  required: true,
213
- help: 'Pick an iMessage contact. Their phone number or email becomes the thread id.',
214
- picker: {
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.',
217
- allowCustom: true,
218
- },
214
+ help: 'Paste the contact\'s iMessage handle exactly as it appears in your Messages thread. The iMessage MCP\'s search_contacts reads the Contacts app via AppleScript and is unreliable — we bypass it by letting you paste the handle directly.',
219
215
  },
220
216
  { key: 'limit', label: 'Max messages per run', placeholder: '50', defaultValue: '50' },
221
217
  ],
@@ -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);
@@ -2654,7 +2659,13 @@ export async function cmdDashboard(opts) {
2654
2659
  }
2655
2660
  const { query } = await import('@anthropic-ai/claude-agent-sdk');
2656
2661
  const { parseJsonResponse } = await import('../brain/llm-client.js');
2662
+ const { getMcpServersForAgent } = await import('../agent/mcp-bridge.js');
2657
2663
  const { MODELS } = await import('../config.js');
2664
+ // Pass the full MCP server map so the one-shot probe can call
2665
+ // Extension-provided tools (iMessage, figma, supabase, etc.).
2666
+ // Without this the SDK only sees built-in + claude_ai_* tools
2667
+ // and every Extension tool call is silently rejected at runtime.
2668
+ const mcpServers = getMcpServersForAgent();
2658
2669
  // Strict prompt: force JSON-only output. The tool lookup goes through
2659
2670
  // the SDK so claude_ai_* and regular MCP servers work uniformly.
2660
2671
  const stream = query({
@@ -2667,6 +2678,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
2667
2678
  maxTurns: 3,
2668
2679
  systemPrompt: 'You are a data enumerator. You call the given tool once, extract the items from its response, and emit a strict JSON array. No commentary.',
2669
2680
  allowedTools: [tool],
2681
+ mcpServers,
2670
2682
  permissionMode: 'bypassPermissions',
2671
2683
  settingSources: [],
2672
2684
  },
@@ -10784,12 +10796,43 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10784
10796
  brainFeedWizardRender();
10785
10797
  }
10786
10798
 
10799
+ // Per-field typeahead state: { [fieldKey]: { timer, lastQuery } }
10800
+ const brainPickerTypeahead = {};
10801
+
10787
10802
  async function brainRenderFieldPicker(field, values) {
10788
10803
  const container = document.querySelector('[data-field-picker="' + field.key + '"]');
10789
10804
  const hidden = document.querySelector('input[type=hidden][data-field="' + field.key + '"]');
10790
10805
  if (!container || !hidden) return;
10791
10806
  const picker = field.picker;
10792
10807
 
10808
+ // Typeahead mode: render a search box. User types → debounce → probe.
10809
+ if (picker.queryArg) {
10810
+ const minLen = picker.minQueryLength || 2;
10811
+ const currentVal = hidden.value || values[field.key] || '';
10812
+ container.innerHTML =
10813
+ '<input type="text" id="brain-picker-search-' + field.key + '" placeholder="Type to search (min ' + minLen + ' chars)…" style="width:100%" value="">' +
10814
+ '<div id="brain-picker-results-' + field.key + '" style="margin-top:6px;max-height:240px;overflow-y:auto"></div>' +
10815
+ (currentVal
10816
+ ? '<div style="font-size:12px;margin-top:6px">Selected: <code>' + escapeHtml(currentVal) + '</code></div>'
10817
+ : '');
10818
+ const searchEl = document.getElementById('brain-picker-search-' + field.key);
10819
+ searchEl.oninput = function() {
10820
+ const q = this.value.trim();
10821
+ if (brainPickerTypeahead[field.key] && brainPickerTypeahead[field.key].timer) {
10822
+ clearTimeout(brainPickerTypeahead[field.key].timer);
10823
+ }
10824
+ if (q.length < minLen) {
10825
+ document.getElementById('brain-picker-results-' + field.key).innerHTML =
10826
+ '<div style="color:var(--muted);font-size:12px;padding:6px">Type at least ' + minLen + ' characters…</div>';
10827
+ return;
10828
+ }
10829
+ brainPickerTypeahead[field.key] = {
10830
+ timer: setTimeout(function() { brainFireTypeaheadProbe(field, q); }, 600),
10831
+ };
10832
+ };
10833
+ return;
10834
+ }
10835
+
10793
10836
  try {
10794
10837
  const resp = await apiFetch('/api/brain/mcp/probe', {
10795
10838
  method: 'POST', headers: { 'content-type': 'application/json' },
@@ -10836,6 +10879,64 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10836
10879
  }
10837
10880
  }
10838
10881
 
10882
+ async function brainFireTypeaheadProbe(field, query) {
10883
+ const resultsEl = document.getElementById('brain-picker-results-' + field.key);
10884
+ const hidden = document.querySelector('input[type=hidden][data-field="' + field.key + '"]');
10885
+ if (!resultsEl || !hidden) return;
10886
+ resultsEl.innerHTML = '<div style="color:var(--muted);font-size:12px;padding:6px">Searching…</div>';
10887
+ try {
10888
+ const resp = await apiFetch('/api/brain/mcp/probe', {
10889
+ method: 'POST', headers: { 'content-type': 'application/json' },
10890
+ body: JSON.stringify({ tool: field.picker.tool, intent: field.picker.intent, query }),
10891
+ });
10892
+ const data = await resp.json();
10893
+ if (!resp.ok) throw new Error(data.error || 'probe failed');
10894
+ const items = data.items || [];
10895
+ if (!items.length) {
10896
+ // If the raw preview is anything other than an empty array (with
10897
+ // or without a markdown code fence), show it so the user can tell
10898
+ // whether the tool actually ran.
10899
+ const trimmed = (data.rawPreview || '').trim();
10900
+ const isEmptyArray = /^\s*\[\s*\]\s*$/.test(trimmed.replace(/^[^\[]*/, '').replace(/[^\]]*$/, ''));
10901
+ const rawHint = trimmed && !isEmptyArray
10902
+ ? ' <span style="color:var(--muted)">Tool said: ' + escapeHtml(trimmed.slice(0, 140)) + '</span>'
10903
+ : '';
10904
+ 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>';
10905
+ return;
10906
+ }
10907
+ resultsEl.innerHTML = items.map(function(it) {
10908
+ const lbl = escapeHtml(it.label) + (it.sublabel ? ' <span style="color:var(--muted);font-size:11px">— ' + escapeHtml(it.sublabel) + '</span>' : '');
10909
+ 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>';
10910
+ }).join('') +
10911
+ '<div style="font-size:11px;color:var(--muted);margin-top:4px">' + items.length + ' result' + (items.length === 1 ? '' : 's') + (data.cached ? ' (cached)' : '') + '</div>';
10912
+ } catch (err) {
10913
+ resultsEl.innerHTML = '<div style="color:#e66;font-size:12px;padding:6px">' + escapeHtml(String(err && err.message ? err.message : err)) + '</div>';
10914
+ }
10915
+ }
10916
+
10917
+ function brainPickTypeaheadItem(fieldKey, encodedId, encodedLabel) {
10918
+ const id = decodeURIComponent(encodedId);
10919
+ const label = decodeURIComponent(encodedLabel);
10920
+ const hidden = document.querySelector('input[type=hidden][data-field="' + fieldKey + '"]');
10921
+ if (hidden) hidden.value = id;
10922
+ // Show a confirmation chip and collapse the results.
10923
+ const container = document.querySelector('[data-field-picker="' + fieldKey + '"]');
10924
+ if (!container) return;
10925
+ const searchEl = document.getElementById('brain-picker-search-' + fieldKey);
10926
+ if (searchEl) searchEl.value = label;
10927
+ const resultsEl = document.getElementById('brain-picker-results-' + fieldKey);
10928
+ if (resultsEl) resultsEl.innerHTML = '';
10929
+ // Inject a "selected" line
10930
+ let sel = container.querySelector('.brain-picker-selected');
10931
+ if (!sel) {
10932
+ sel = document.createElement('div');
10933
+ sel.className = 'brain-picker-selected';
10934
+ sel.style.cssText = 'font-size:12px;margin-top:6px';
10935
+ container.appendChild(sel);
10936
+ }
10937
+ sel.innerHTML = '✓ Selected: <b>' + escapeHtml(label) + '</b> <code style="font-size:11px;color:var(--muted)">' + escapeHtml(id) + '</code>';
10938
+ }
10939
+
10839
10940
  function brainFieldPickerToggleCustom(fieldKey, encodedPicker) {
10840
10941
  const container = document.querySelector('[data-field-picker="' + fieldKey + '"]');
10841
10942
  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.75",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",