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: '
|
|
211
|
+
label: 'Phone number or email',
|
|
212
|
+
placeholder: '+15551234567 or someone@example.com',
|
|
212
213
|
required: true,
|
|
213
|
-
help: '
|
|
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
|
],
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -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
|
|
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 (!
|
|
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 + '"]');
|