clementine-agent 1.12.1 → 1.12.3
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.
package/dist/cli/dashboard.js
CHANGED
|
@@ -4555,6 +4555,21 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4555
4555
|
res.status(500).json({ error: String(err) });
|
|
4556
4556
|
}
|
|
4557
4557
|
});
|
|
4558
|
+
// Force-refresh the Composio caches. Useful when the user just made a
|
|
4559
|
+
// connection in Composio's web UI and wants the agent to pick it up
|
|
4560
|
+
// immediately instead of waiting up to 60s for the user_id cache to
|
|
4561
|
+
// expire. Agents can also call this after surfacing a "needs OAuth"
|
|
4562
|
+
// error to retry cleanly.
|
|
4563
|
+
app.post('/api/composio/refresh', async (_req, res) => {
|
|
4564
|
+
try {
|
|
4565
|
+
const c = await import('../integrations/composio/client.js');
|
|
4566
|
+
c.resetComposioClient();
|
|
4567
|
+
res.json({ ok: true, message: 'Composio caches cleared — next query will re-detect.' });
|
|
4568
|
+
}
|
|
4569
|
+
catch (err) {
|
|
4570
|
+
res.status(500).json({ error: String(err) });
|
|
4571
|
+
}
|
|
4572
|
+
});
|
|
4558
4573
|
// ── CRON CRUD routes ──────────────────────────────────────────
|
|
4559
4574
|
app.get('/api/projects', (_req, res) => {
|
|
4560
4575
|
try {
|
|
@@ -15444,7 +15459,14 @@ function navigateTo(page, opts) {
|
|
|
15444
15459
|
if (bt === 'learning' && typeof refreshSelfImprove === 'function') refreshSelfImprove();
|
|
15445
15460
|
break;
|
|
15446
15461
|
case 'settings':
|
|
15447
|
-
|
|
15462
|
+
// Settings tabs use the switchTab() system (id="tab-settings-<tab>"),
|
|
15463
|
+
// not switchDestTab's [data-tab] selector. Default tab name is
|
|
15464
|
+
// 'general' (the "Channels & Env" pane) to match the HTML id.
|
|
15465
|
+
switchTab('settings', opts.tab || 'general');
|
|
15466
|
+
// 'general' has no auto-refresh in switchTab — kick it manually so
|
|
15467
|
+
// the Channels & Env card actually loads from /api/settings instead
|
|
15468
|
+
// of stalling on "Loading settings…".
|
|
15469
|
+
if ((opts.tab || 'general') === 'general' && typeof refreshSettings === 'function') refreshSettings();
|
|
15448
15470
|
break;
|
|
15449
15471
|
}
|
|
15450
15472
|
|
|
@@ -15846,6 +15868,7 @@ function switchTab(group, tab) {
|
|
|
15846
15868
|
if (tab === 'learning' && typeof refreshSelfImprove === 'function') refreshSelfImprove();
|
|
15847
15869
|
}
|
|
15848
15870
|
if (group === 'settings') {
|
|
15871
|
+
if (tab === 'general' && typeof refreshSettings === 'function') refreshSettings();
|
|
15849
15872
|
if (tab === 'integrations') { refreshSalesforce(); refreshComposioConnections(); }
|
|
15850
15873
|
if (tab === 'remote') refreshRemoteAccess();
|
|
15851
15874
|
if (tab === 'security') refreshAuthSessions();
|
|
@@ -25936,9 +25959,13 @@ async function saveComposioApiKey() {
|
|
|
25936
25959
|
}
|
|
25937
25960
|
|
|
25938
25961
|
async function connectComposio(slug) {
|
|
25962
|
+
// Open a blank popup SYNCHRONOUSLY inside the click handler. Browsers
|
|
25963
|
+
// only let popups through if they're a direct user gesture — once we
|
|
25964
|
+
// hit the await below, the gesture is "consumed" and any later
|
|
25965
|
+
// window.open() gets silently blocked by Chrome/Safari/Firefox.
|
|
25966
|
+
// We'll redirect this blank window to the OAuth URL once we have it.
|
|
25967
|
+
var popup = window.open('about:blank', '_blank');
|
|
25939
25968
|
try {
|
|
25940
|
-
// Use apiFetch (not raw fetch) so the Authorization: Bearer header is
|
|
25941
|
-
// attached — the /api/* middleware rejects unauth'd POSTs with 401.
|
|
25942
25969
|
var res = await apiFetch('/api/composio/toolkits/' + encodeURIComponent(slug) + '/authorize', {
|
|
25943
25970
|
method: 'POST',
|
|
25944
25971
|
headers: { 'content-type': 'application/json' },
|
|
@@ -25947,27 +25974,61 @@ async function connectComposio(slug) {
|
|
|
25947
25974
|
var d = await res.json();
|
|
25948
25975
|
if (res.status === 409 && d.needsAuthConfig) {
|
|
25949
25976
|
toast('This toolkit needs a BYO OAuth app — opening Composio dashboard.', 'warn');
|
|
25950
|
-
|
|
25977
|
+
if (popup && !popup.closed) popup.location.href = d.setupUrl;
|
|
25978
|
+
else showOAuthLinkPrompt(slug, d.setupUrl);
|
|
25951
25979
|
return;
|
|
25952
25980
|
}
|
|
25953
25981
|
if (!res.ok) {
|
|
25982
|
+
if (popup && !popup.closed) popup.close();
|
|
25954
25983
|
var reason = d.error || ('HTTP ' + res.status);
|
|
25955
25984
|
toast('Connect failed: ' + reason, 'error');
|
|
25956
25985
|
console.error('[composio] connect failed', { slug: slug, status: res.status, body: d });
|
|
25957
25986
|
return;
|
|
25958
25987
|
}
|
|
25959
25988
|
if (d.redirectUrl) {
|
|
25960
|
-
|
|
25961
|
-
|
|
25962
|
-
|
|
25989
|
+
if (popup && !popup.closed) {
|
|
25990
|
+
popup.location.href = d.redirectUrl;
|
|
25991
|
+
toast('Authorize ' + slug + ' in the new tab, then come back here.', 'info');
|
|
25992
|
+
} else {
|
|
25993
|
+
// Popup got blocked even with the synchronous-open trick (some
|
|
25994
|
+
// browsers / extensions block about:blank too). Fall back to a
|
|
25995
|
+
// visible "click to open" dialog — that click is a direct gesture
|
|
25996
|
+
// and reliably bypasses the blocker.
|
|
25997
|
+
showOAuthLinkPrompt(slug, d.redirectUrl);
|
|
25998
|
+
}
|
|
25963
25999
|
setTimeout(refreshComposioConnections, 5000);
|
|
25964
26000
|
setTimeout(refreshComposioConnections, 15000);
|
|
25965
26001
|
setTimeout(refreshComposioConnections, 30000);
|
|
25966
26002
|
} else {
|
|
26003
|
+
if (popup && !popup.closed) popup.close();
|
|
25967
26004
|
toast('Connected ' + slug, 'success');
|
|
25968
26005
|
refreshComposioConnections();
|
|
25969
26006
|
}
|
|
25970
|
-
} catch (e) {
|
|
26007
|
+
} catch (e) {
|
|
26008
|
+
if (popup && !popup.closed) popup.close();
|
|
26009
|
+
toast('Connect failed: ' + e, 'error');
|
|
26010
|
+
}
|
|
26011
|
+
}
|
|
26012
|
+
|
|
26013
|
+
function showOAuthLinkPrompt(slug, url) {
|
|
26014
|
+
// Renders a modal with a clickable link. User clicking the link is a
|
|
26015
|
+
// direct gesture, so the new tab will open even when popup blockers are
|
|
26016
|
+
// aggressive. Used as the fallback when window.open returns null.
|
|
26017
|
+
var existing = document.getElementById('composio-oauth-prompt');
|
|
26018
|
+
if (existing) existing.remove();
|
|
26019
|
+
var overlay = document.createElement('div');
|
|
26020
|
+
overlay.id = 'composio-oauth-prompt';
|
|
26021
|
+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center';
|
|
26022
|
+
overlay.innerHTML =
|
|
26023
|
+
'<div style="background:var(--bg-primary,#1e1e1e);border:1px solid var(--border);border-radius:8px;padding:20px;max-width:480px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.4)">' +
|
|
26024
|
+
'<div style="font-size:14px;font-weight:600;margin-bottom:8px">Authorize ' + esc(slug) + '</div>' +
|
|
26025
|
+
'<div style="font-size:12px;color:var(--text-muted);margin-bottom:14px;line-height:1.5">Your browser blocked the popup. Click below to open the authorization page in a new tab — after approving, come back here and the connection will show up within a few seconds.</div>' +
|
|
26026
|
+
'<div style="display:flex;gap:8px;justify-content:flex-end">' +
|
|
26027
|
+
'<button class="btn-sm" onclick="document.getElementById(\\'composio-oauth-prompt\\').remove()" style="padding:6px 12px;background:transparent;border:1px solid var(--border);color:var(--text-primary);border-radius:4px;cursor:pointer;font-size:12px">Cancel</button>' +
|
|
26028
|
+
'<a href="' + esc(url) + '" target="_blank" rel="noopener" onclick="setTimeout(function(){var e=document.getElementById(\\'composio-oauth-prompt\\');if(e)e.remove();},500)" class="btn btn-sm btn-primary" style="text-decoration:none;padding:6px 14px;display:inline-block;font-size:12px">Open authorization</a>' +
|
|
26029
|
+
'</div>' +
|
|
26030
|
+
'</div>';
|
|
26031
|
+
document.body.appendChild(overlay);
|
|
25971
26032
|
}
|
|
25972
26033
|
|
|
25973
26034
|
async function disconnectComposio(slug, connectionId) {
|
|
@@ -108,13 +108,22 @@ export function clementineUserId() {
|
|
|
108
108
|
return readComposioEnv('COMPOSIO_USER_ID') || DEFAULT_NEW_CONNECTION_USER_ID;
|
|
109
109
|
}
|
|
110
110
|
// Cached after first detection — avoids extra API calls per authorize.
|
|
111
|
+
// Cache the detected user_id but only briefly. New connections made via
|
|
112
|
+
// Composio's web UI (outside Clementine) need to be picked up without a
|
|
113
|
+
// daemon restart — long-lived caching breaks that. 60s is short enough
|
|
114
|
+
// for "hit dashboard → connect in Composio web → use it from agent" to
|
|
115
|
+
// work without explicit invalidation, and long enough that within-burst
|
|
116
|
+
// queries don't hammer the API.
|
|
111
117
|
let detectedPreferredUserId = null;
|
|
118
|
+
let detectedAt = 0;
|
|
119
|
+
const USER_ID_CACHE_TTL_MS = 60_000;
|
|
112
120
|
async function detectPreferredUserId(composio) {
|
|
113
121
|
const explicit = readComposioEnv('COMPOSIO_USER_ID');
|
|
114
122
|
if (explicit)
|
|
115
123
|
return explicit;
|
|
116
|
-
if (detectedPreferredUserId !== null)
|
|
124
|
+
if (detectedPreferredUserId !== null && Date.now() - detectedAt < USER_ID_CACHE_TTL_MS) {
|
|
117
125
|
return detectedPreferredUserId;
|
|
126
|
+
}
|
|
118
127
|
// The high-level wrapper's list() drops the snake_case `user_id` field
|
|
119
128
|
// during its camelCase transformation, so connections look like they have
|
|
120
129
|
// no user_id. Use the raw client (snake_case shape) to actually read the
|
|
@@ -141,6 +150,7 @@ async function detectPreferredUserId(composio) {
|
|
|
141
150
|
const top = [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
142
151
|
logger.info({ userId: top, candidates: counts.size }, 'Detected Composio user_id from existing connections');
|
|
143
152
|
detectedPreferredUserId = top;
|
|
153
|
+
detectedAt = Date.now();
|
|
144
154
|
return top;
|
|
145
155
|
}
|
|
146
156
|
}
|
|
@@ -151,6 +161,7 @@ async function detectPreferredUserId(composio) {
|
|
|
151
161
|
// requires a non-empty string, and "default" is the conventional
|
|
152
162
|
// single-tenant value that Composio's quickstart uses.
|
|
153
163
|
detectedPreferredUserId = DEFAULT_NEW_CONNECTION_USER_ID;
|
|
164
|
+
detectedAt = Date.now();
|
|
154
165
|
return DEFAULT_NEW_CONNECTION_USER_ID;
|
|
155
166
|
}
|
|
156
167
|
export function displayNameFor(slug) {
|
|
@@ -517,6 +528,10 @@ _opts) {
|
|
|
517
528
|
try {
|
|
518
529
|
const conn = await composio.toolkits.authorize(userId, slug);
|
|
519
530
|
logger.info({ slug, userId, connectionId: conn.id }, 'Composio authorize OK');
|
|
531
|
+
// Force re-detect on the next query so the new connection (and any
|
|
532
|
+
// others created in parallel via Composio's web UI) get picked up
|
|
533
|
+
// immediately, even within the 60s TTL window.
|
|
534
|
+
detectedPreferredUserId = null;
|
|
520
535
|
return { redirectUrl: conn.redirectUrl ?? null, connectionId: conn.id };
|
|
521
536
|
}
|
|
522
537
|
catch (err) {
|