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.
@@ -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
- const [catalog, connected, configured] = await Promise.all([
4410
- c.listAllToolkits(),
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
- switchDestTab('settings', opts.tab || 'channels');
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
- container.innerHTML = '<div class="empty-state" style="padding:16px">No toolkits available. Check that your Composio API key is valid.</div>';
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
- window.open(d.setupUrl, '_blank');
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
- window.open(d.redirectUrl, '_blank');
25941
- toast('Opened ' + slug + ' authorization in a new tab. Refresh after approving.', 'info');
25942
- // Soft poll to catch the new connection without forcing a manual refresh.
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) { toast('Connect failed: ' + e, 'error'); }
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; resetComposioClient() clears
83
- * the cache when the API key changes.
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; resetComposioClient() clears
346
- * the cache when the API key changes.
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
- const out = [];
357
- let cursor;
358
- // Bounded loop 30 pages × 500 items = 15K ceiling, far more than any
359
- // realistic catalog. Composio currently returns ~1.5K items across 3
360
- // pages, but the cap makes a runaway impossible.
361
- for (let page = 0; page < 30; page++) {
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
- const rawClient = composio.client;
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
- logger.warn({ err, page }, 'listAllToolkits page failed — stopping pagination');
389
- break;
373
+ lastError = err;
374
+ logger.error({ err }, 'High-level toolkit list also failed');
390
375
  }
391
376
  }
392
- catalogCache = { at: now, data: out };
393
- logger.info({ count: out.length }, 'Composio catalog fetched');
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.12.0",
3
+ "version": "1.12.2",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",