clementine-agent 1.12.0 → 1.12.2
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
|
@@ -4406,8 +4406,16 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4406
4406
|
// This replaces the hardcoded CURATED_TOOLKITS — slug typos are now
|
|
4407
4407
|
// impossible because we render directly from Composio's data, and
|
|
4408
4408
|
// newly-added services appear automatically.
|
|
4409
|
-
|
|
4410
|
-
|
|
4409
|
+
let catalog = [];
|
|
4410
|
+
let catalogError = null;
|
|
4411
|
+
try {
|
|
4412
|
+
catalog = await c.listAllToolkits();
|
|
4413
|
+
}
|
|
4414
|
+
catch (err) {
|
|
4415
|
+
catalogError = err?.message ?? String(err);
|
|
4416
|
+
console.error('[composio] catalog fetch failed:', catalogError);
|
|
4417
|
+
}
|
|
4418
|
+
const [connected, configured] = await Promise.all([
|
|
4411
4419
|
c.listConnectedToolkits(),
|
|
4412
4420
|
c.listToolkitSlugsWithAuthConfig(),
|
|
4413
4421
|
]);
|
|
@@ -4475,6 +4483,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4475
4483
|
toolkits,
|
|
4476
4484
|
featured: featured.map(t => t.slug),
|
|
4477
4485
|
totalCount: toolkits.length,
|
|
4486
|
+
catalogError,
|
|
4478
4487
|
});
|
|
4479
4488
|
}
|
|
4480
4489
|
catch (err) {
|
|
@@ -15435,7 +15444,14 @@ function navigateTo(page, opts) {
|
|
|
15435
15444
|
if (bt === 'learning' && typeof refreshSelfImprove === 'function') refreshSelfImprove();
|
|
15436
15445
|
break;
|
|
15437
15446
|
case 'settings':
|
|
15438
|
-
|
|
15447
|
+
// Settings tabs use the switchTab() system (id="tab-settings-<tab>"),
|
|
15448
|
+
// not switchDestTab's [data-tab] selector. Default tab name is
|
|
15449
|
+
// 'general' (the "Channels & Env" pane) to match the HTML id.
|
|
15450
|
+
switchTab('settings', opts.tab || 'general');
|
|
15451
|
+
// 'general' has no auto-refresh in switchTab — kick it manually so
|
|
15452
|
+
// the Channels & Env card actually loads from /api/settings instead
|
|
15453
|
+
// of stalling on "Loading settings…".
|
|
15454
|
+
if ((opts.tab || 'general') === 'general' && typeof refreshSettings === 'function') refreshSettings();
|
|
15439
15455
|
break;
|
|
15440
15456
|
}
|
|
15441
15457
|
|
|
@@ -15837,6 +15853,7 @@ function switchTab(group, tab) {
|
|
|
15837
15853
|
if (tab === 'learning' && typeof refreshSelfImprove === 'function') refreshSelfImprove();
|
|
15838
15854
|
}
|
|
15839
15855
|
if (group === 'settings') {
|
|
15856
|
+
if (tab === 'general' && typeof refreshSettings === 'function') refreshSettings();
|
|
15840
15857
|
if (tab === 'integrations') { refreshSalesforce(); refreshComposioConnections(); }
|
|
15841
15858
|
if (tab === 'remote') refreshRemoteAccess();
|
|
15842
15859
|
if (tab === 'security') refreshAuthSessions();
|
|
@@ -25794,7 +25811,18 @@ async function refreshComposioConnections() {
|
|
|
25794
25811
|
}
|
|
25795
25812
|
var toolkits = d.toolkits || [];
|
|
25796
25813
|
if (toolkits.length === 0) {
|
|
25797
|
-
|
|
25814
|
+
// Distinguish "catalog fetch failed" from "user has nothing connected".
|
|
25815
|
+
// Surfacing d.catalogError tells us if Composio rejected the catalog
|
|
25816
|
+
// call (different permission than connectedAccounts.list, etc.).
|
|
25817
|
+
var msg = d.catalogError
|
|
25818
|
+
? 'Could not load toolkits from Composio: <code style="color:var(--red);font-size:11px">' + esc(d.catalogError) + '</code>'
|
|
25819
|
+
: 'Composio returned an empty toolkit catalog. This is unusual — try refreshing.';
|
|
25820
|
+
container.innerHTML =
|
|
25821
|
+
'<div style="padding:16px">' +
|
|
25822
|
+
'<div style="margin-bottom:12px;font-size:13px">' + msg + '</div>' +
|
|
25823
|
+
'<div style="font-size:11px;color:var(--text-muted);margin-bottom:12px">If your existing connections (e.g. Outlook) work via the agent, your API key is valid — the catalog endpoint may require a different permission tier or there\\'s a transient API issue.</div>' +
|
|
25824
|
+
'<button class="btn btn-sm btn-primary" onclick="refreshComposioConnections()">Retry</button>' +
|
|
25825
|
+
'</div>';
|
|
25798
25826
|
return;
|
|
25799
25827
|
}
|
|
25800
25828
|
// Stash full catalog in a closure so the search input can re-render
|
|
@@ -25916,9 +25944,13 @@ async function saveComposioApiKey() {
|
|
|
25916
25944
|
}
|
|
25917
25945
|
|
|
25918
25946
|
async function connectComposio(slug) {
|
|
25947
|
+
// Open a blank popup SYNCHRONOUSLY inside the click handler. Browsers
|
|
25948
|
+
// only let popups through if they're a direct user gesture — once we
|
|
25949
|
+
// hit the await below, the gesture is "consumed" and any later
|
|
25950
|
+
// window.open() gets silently blocked by Chrome/Safari/Firefox.
|
|
25951
|
+
// We'll redirect this blank window to the OAuth URL once we have it.
|
|
25952
|
+
var popup = window.open('about:blank', '_blank');
|
|
25919
25953
|
try {
|
|
25920
|
-
// Use apiFetch (not raw fetch) so the Authorization: Bearer header is
|
|
25921
|
-
// attached — the /api/* middleware rejects unauth'd POSTs with 401.
|
|
25922
25954
|
var res = await apiFetch('/api/composio/toolkits/' + encodeURIComponent(slug) + '/authorize', {
|
|
25923
25955
|
method: 'POST',
|
|
25924
25956
|
headers: { 'content-type': 'application/json' },
|
|
@@ -25927,27 +25959,61 @@ async function connectComposio(slug) {
|
|
|
25927
25959
|
var d = await res.json();
|
|
25928
25960
|
if (res.status === 409 && d.needsAuthConfig) {
|
|
25929
25961
|
toast('This toolkit needs a BYO OAuth app — opening Composio dashboard.', 'warn');
|
|
25930
|
-
|
|
25962
|
+
if (popup && !popup.closed) popup.location.href = d.setupUrl;
|
|
25963
|
+
else showOAuthLinkPrompt(slug, d.setupUrl);
|
|
25931
25964
|
return;
|
|
25932
25965
|
}
|
|
25933
25966
|
if (!res.ok) {
|
|
25967
|
+
if (popup && !popup.closed) popup.close();
|
|
25934
25968
|
var reason = d.error || ('HTTP ' + res.status);
|
|
25935
25969
|
toast('Connect failed: ' + reason, 'error');
|
|
25936
25970
|
console.error('[composio] connect failed', { slug: slug, status: res.status, body: d });
|
|
25937
25971
|
return;
|
|
25938
25972
|
}
|
|
25939
25973
|
if (d.redirectUrl) {
|
|
25940
|
-
|
|
25941
|
-
|
|
25942
|
-
|
|
25974
|
+
if (popup && !popup.closed) {
|
|
25975
|
+
popup.location.href = d.redirectUrl;
|
|
25976
|
+
toast('Authorize ' + slug + ' in the new tab, then come back here.', 'info');
|
|
25977
|
+
} else {
|
|
25978
|
+
// Popup got blocked even with the synchronous-open trick (some
|
|
25979
|
+
// browsers / extensions block about:blank too). Fall back to a
|
|
25980
|
+
// visible "click to open" dialog — that click is a direct gesture
|
|
25981
|
+
// and reliably bypasses the blocker.
|
|
25982
|
+
showOAuthLinkPrompt(slug, d.redirectUrl);
|
|
25983
|
+
}
|
|
25943
25984
|
setTimeout(refreshComposioConnections, 5000);
|
|
25944
25985
|
setTimeout(refreshComposioConnections, 15000);
|
|
25945
25986
|
setTimeout(refreshComposioConnections, 30000);
|
|
25946
25987
|
} else {
|
|
25988
|
+
if (popup && !popup.closed) popup.close();
|
|
25947
25989
|
toast('Connected ' + slug, 'success');
|
|
25948
25990
|
refreshComposioConnections();
|
|
25949
25991
|
}
|
|
25950
|
-
} catch (e) {
|
|
25992
|
+
} catch (e) {
|
|
25993
|
+
if (popup && !popup.closed) popup.close();
|
|
25994
|
+
toast('Connect failed: ' + e, 'error');
|
|
25995
|
+
}
|
|
25996
|
+
}
|
|
25997
|
+
|
|
25998
|
+
function showOAuthLinkPrompt(slug, url) {
|
|
25999
|
+
// Renders a modal with a clickable link. User clicking the link is a
|
|
26000
|
+
// direct gesture, so the new tab will open even when popup blockers are
|
|
26001
|
+
// aggressive. Used as the fallback when window.open returns null.
|
|
26002
|
+
var existing = document.getElementById('composio-oauth-prompt');
|
|
26003
|
+
if (existing) existing.remove();
|
|
26004
|
+
var overlay = document.createElement('div');
|
|
26005
|
+
overlay.id = 'composio-oauth-prompt';
|
|
26006
|
+
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';
|
|
26007
|
+
overlay.innerHTML =
|
|
26008
|
+
'<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)">' +
|
|
26009
|
+
'<div style="font-size:14px;font-weight:600;margin-bottom:8px">Authorize ' + esc(slug) + '</div>' +
|
|
26010
|
+
'<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>' +
|
|
26011
|
+
'<div style="display:flex;gap:8px;justify-content:flex-end">' +
|
|
26012
|
+
'<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>' +
|
|
26013
|
+
'<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>' +
|
|
26014
|
+
'</div>' +
|
|
26015
|
+
'</div>';
|
|
26016
|
+
document.body.appendChild(overlay);
|
|
25951
26017
|
}
|
|
25952
26018
|
|
|
25953
26019
|
async function disconnectComposio(slug, connectionId) {
|
|
@@ -79,8 +79,8 @@ export interface CatalogToolkit {
|
|
|
79
79
|
* static CURATED_TOOLKITS array as the source of truth for the dashboard
|
|
80
80
|
* Connections panel — slug typos are now impossible because we render from
|
|
81
81
|
* Composio's own data, and new services appear automatically as they're
|
|
82
|
-
* added. Cached at module level for 1 hour;
|
|
83
|
-
*
|
|
82
|
+
* added. Cached at module level for 1 hour on success; failures don't
|
|
83
|
+
* cache (so the next request retries instead of returning stale empty).
|
|
84
84
|
*/
|
|
85
85
|
export declare function listAllToolkits(): Promise<CatalogToolkit[]>;
|
|
86
86
|
export declare function listToolkitMeta(): Promise<Map<string, ToolkitMeta>>;
|
|
@@ -342,57 +342,88 @@ const CATALOG_TTL_MS = 60 * 60 * 1000; // 1h — catalog drifts very slowly
|
|
|
342
342
|
* static CURATED_TOOLKITS array as the source of truth for the dashboard
|
|
343
343
|
* Connections panel — slug typos are now impossible because we render from
|
|
344
344
|
* Composio's own data, and new services appear automatically as they're
|
|
345
|
-
* added. Cached at module level for 1 hour;
|
|
346
|
-
*
|
|
345
|
+
* added. Cached at module level for 1 hour on success; failures don't
|
|
346
|
+
* cache (so the next request retries instead of returning stale empty).
|
|
347
347
|
*/
|
|
348
348
|
export async function listAllToolkits() {
|
|
349
349
|
const now = Date.now();
|
|
350
|
-
if (catalogCache && now - catalogCache.at < CATALOG_TTL_MS) {
|
|
350
|
+
if (catalogCache && catalogCache.data.length > 0 && now - catalogCache.at < CATALOG_TTL_MS) {
|
|
351
351
|
return catalogCache.data;
|
|
352
352
|
}
|
|
353
353
|
const composio = getComposio();
|
|
354
354
|
if (!composio)
|
|
355
355
|
return [];
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
356
|
+
// Try the raw client first — supports cursor pagination so we get the full
|
|
357
|
+
// 1000+ catalog. If that errors (some API keys / plans restrict it), fall
|
|
358
|
+
// back to the high-level wrapper which returns up to 500 in one shot.
|
|
359
|
+
let result = [];
|
|
360
|
+
let lastError = null;
|
|
361
|
+
try {
|
|
362
|
+
result = await fetchCatalogViaRawClient(composio);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
lastError = err;
|
|
366
|
+
logger.warn({ err }, 'Raw-client toolkit list failed — falling back to high-level wrapper');
|
|
367
|
+
}
|
|
368
|
+
if (result.length === 0) {
|
|
362
369
|
try {
|
|
363
|
-
|
|
364
|
-
const resp = await rawClient.toolkits.list({
|
|
365
|
-
limit: 500,
|
|
366
|
-
...(cursor ? { cursor } : {}),
|
|
367
|
-
});
|
|
368
|
-
const items = (resp?.items ?? []);
|
|
369
|
-
for (const it of items) {
|
|
370
|
-
const managed = (it.composioManagedAuthSchemes ?? it.composio_managed_auth_schemes ?? []);
|
|
371
|
-
const schemes = (it.authSchemes ?? it.auth_schemes ?? []);
|
|
372
|
-
const noAuth = it.noAuth ?? it.no_auth ?? false;
|
|
373
|
-
out.push({
|
|
374
|
-
slug: it.slug,
|
|
375
|
-
name: it.name,
|
|
376
|
-
logoUrl: it.meta?.logo,
|
|
377
|
-
description: it.meta?.description,
|
|
378
|
-
toolsCount: it.meta?.toolsCount ?? it.meta?.tools_count,
|
|
379
|
-
authMode: noAuth ? 'none' : (managed.length > 0 ? 'managed' : (schemes.length > 0 ? 'byo' : 'none')),
|
|
380
|
-
categories: it.meta?.categories ?? [],
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
cursor = resp?.next_cursor ?? resp?.nextCursor;
|
|
384
|
-
if (!cursor || items.length === 0)
|
|
385
|
-
break;
|
|
370
|
+
result = await fetchCatalogViaWrapper(composio);
|
|
386
371
|
}
|
|
387
372
|
catch (err) {
|
|
388
|
-
|
|
389
|
-
|
|
373
|
+
lastError = err;
|
|
374
|
+
logger.error({ err }, 'High-level toolkit list also failed');
|
|
390
375
|
}
|
|
391
376
|
}
|
|
392
|
-
|
|
393
|
-
|
|
377
|
+
if (result.length === 0) {
|
|
378
|
+
// Surface whichever error we hit so the dashboard can render something
|
|
379
|
+
// actionable instead of "no toolkits available".
|
|
380
|
+
const detail = lastError?.message ?? 'Unknown error';
|
|
381
|
+
throw new Error(`Composio catalog fetch failed: ${detail}`);
|
|
382
|
+
}
|
|
383
|
+
catalogCache = { at: now, data: result };
|
|
384
|
+
logger.info({ count: result.length }, 'Composio catalog fetched');
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
function normalizeCatalogItem(it) {
|
|
388
|
+
const managed = (it.composioManagedAuthSchemes ?? it.composio_managed_auth_schemes ?? []);
|
|
389
|
+
const schemes = (it.authSchemes ?? it.auth_schemes ?? []);
|
|
390
|
+
const noAuth = it.noAuth ?? it.no_auth ?? false;
|
|
391
|
+
return {
|
|
392
|
+
slug: it.slug,
|
|
393
|
+
name: it.name,
|
|
394
|
+
logoUrl: it.meta?.logo,
|
|
395
|
+
description: it.meta?.description,
|
|
396
|
+
toolsCount: it.meta?.toolsCount ?? it.meta?.tools_count,
|
|
397
|
+
authMode: noAuth ? 'none' : (managed.length > 0 ? 'managed' : (schemes.length > 0 ? 'byo' : 'none')),
|
|
398
|
+
categories: it.meta?.categories ?? [],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function fetchCatalogViaRawClient(composio) {
|
|
402
|
+
const out = [];
|
|
403
|
+
let cursor;
|
|
404
|
+
// Bounded loop — 30 pages × 500 items = 15K ceiling.
|
|
405
|
+
for (let page = 0; page < 30; page++) {
|
|
406
|
+
const rawClient = composio.client;
|
|
407
|
+
const resp = await rawClient.toolkits.list({
|
|
408
|
+
limit: 500,
|
|
409
|
+
...(cursor ? { cursor } : {}),
|
|
410
|
+
});
|
|
411
|
+
const items = (resp?.items ?? []);
|
|
412
|
+
for (const it of items)
|
|
413
|
+
out.push(normalizeCatalogItem(it));
|
|
414
|
+
cursor = resp?.next_cursor ?? resp?.nextCursor;
|
|
415
|
+
if (!cursor || items.length === 0)
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
394
418
|
return out;
|
|
395
419
|
}
|
|
420
|
+
async function fetchCatalogViaWrapper(composio) {
|
|
421
|
+
// High-level wrapper returns an array (up to limit). No cursor support
|
|
422
|
+
// through this path, so we cap at the documented max. Better than zero.
|
|
423
|
+
const resp = await composio.toolkits.get({ limit: 500 });
|
|
424
|
+
const items = (Array.isArray(resp) ? resp : (resp?.items ?? []));
|
|
425
|
+
return items.map(normalizeCatalogItem);
|
|
426
|
+
}
|
|
396
427
|
async function fetchAllToolkitMeta() {
|
|
397
428
|
const composio = getComposio();
|
|
398
429
|
if (!composio)
|