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: '
|
|
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
|
},
|
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);
|
|
@@ -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 + '"]');
|