clementine-agent 1.11.3 → 1.12.0

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.
@@ -4402,10 +4402,14 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
4402
4402
  res.json({ enabled: false, toolkits: [] });
4403
4403
  return;
4404
4404
  }
4405
- const [connected, configured, meta] = await Promise.all([
4405
+ // Fetch live: full catalog + user's connections + auth-config slugs.
4406
+ // This replaces the hardcoded CURATED_TOOLKITS — slug typos are now
4407
+ // impossible because we render directly from Composio's data, and
4408
+ // newly-added services appear automatically.
4409
+ const [catalog, connected, configured] = await Promise.all([
4410
+ c.listAllToolkits(),
4406
4411
  c.listConnectedToolkits(),
4407
4412
  c.listToolkitSlugsWithAuthConfig(),
4408
- c.listToolkitMeta(),
4409
4413
  ]);
4410
4414
  const connectionsBySlug = new Map();
4411
4415
  for (const conn of connected) {
@@ -4426,39 +4430,52 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
4426
4430
  accountAvatarUrl: conn.accountAvatarUrl ?? null,
4427
4431
  createdAt: conn.createdAt ?? null,
4428
4432
  });
4429
- const curated = c.CURATED_TOOLKITS.map(t => {
4430
- const m = meta.get(t.slug);
4431
- const conns = connectionsBySlug.get(t.slug) ?? [];
4432
- return {
4433
+ // Merge: every catalog entry, plus any connection whose slug isn't in
4434
+ // the catalog (defensive — catalog dedupes are rare but possible).
4435
+ const catalogBySlug = new Map(catalog.map(t => [t.slug, t]));
4436
+ const orphanSlugs = [...connectionsBySlug.keys()].filter(s => !catalogBySlug.has(s));
4437
+ const toolkits = [
4438
+ ...catalog.map(t => ({
4433
4439
  slug: t.slug,
4434
- displayName: t.displayName,
4440
+ displayName: t.name,
4435
4441
  authMode: t.authMode,
4436
4442
  hasAuthConfig: configured.has(t.slug),
4437
- logoUrl: m?.logo ?? null,
4438
- description: m?.description ?? null,
4439
- toolCount: m?.toolsCount ?? null,
4440
- connections: conns.map(toView),
4441
- };
4442
- });
4443
- const extras = [...connectionsBySlug.entries()]
4444
- .filter(([slug]) => !c.CURATED_TOOLKITS.some(t => t.slug === slug))
4445
- .map(([slug, conns]) => {
4446
- const m = meta.get(slug);
4447
- // Non-curated: infer auth mode. If a custom auth config exists,
4448
- // user set it up themselves (BYO). Otherwise it must be managed.
4449
- const authMode = configured.has(slug) ? 'byo' : 'managed';
4450
- return {
4443
+ logoUrl: t.logoUrl ?? null,
4444
+ description: t.description ?? null,
4445
+ toolCount: t.toolsCount ?? null,
4446
+ categories: t.categories,
4447
+ connections: (connectionsBySlug.get(t.slug) ?? []).map(toView),
4448
+ })),
4449
+ ...orphanSlugs.map(slug => ({
4451
4450
  slug,
4452
- displayName: m?.name ?? c.displayNameFor(slug),
4453
- authMode,
4451
+ displayName: c.displayNameFor(slug),
4452
+ authMode: 'managed',
4454
4453
  hasAuthConfig: configured.has(slug),
4455
- logoUrl: m?.logo ?? null,
4456
- description: m?.description ?? null,
4457
- toolCount: m?.toolsCount ?? null,
4458
- connections: conns.map(toView),
4459
- };
4454
+ logoUrl: null,
4455
+ description: null,
4456
+ toolCount: null,
4457
+ categories: [],
4458
+ connections: (connectionsBySlug.get(slug) ?? []).map(toView),
4459
+ })),
4460
+ ];
4461
+ // Featured set = toolkits with active connections (highest signal),
4462
+ // then top toolkits by tool count. Cap at 30 to keep the default
4463
+ // grid focused; the search box covers the rest.
4464
+ const FEATURED_LIMIT = 30;
4465
+ const connectedSlugs = new Set(connectionsBySlug.keys());
4466
+ const featured = [
4467
+ ...toolkits.filter(t => connectedSlugs.has(t.slug)),
4468
+ ...toolkits
4469
+ .filter(t => !connectedSlugs.has(t.slug))
4470
+ .sort((a, b) => (b.toolCount ?? 0) - (a.toolCount ?? 0))
4471
+ .slice(0, Math.max(0, FEATURED_LIMIT - connectedSlugs.size)),
4472
+ ];
4473
+ res.json({
4474
+ enabled: true,
4475
+ toolkits,
4476
+ featured: featured.map(t => t.slug),
4477
+ totalCount: toolkits.length,
4460
4478
  });
4461
- res.json({ enabled: true, toolkits: [...curated, ...extras] });
4462
4479
  }
4463
4480
  catch (err) {
4464
4481
  res.status(500).json({ error: String(err) });
@@ -25780,53 +25797,104 @@ async function refreshComposioConnections() {
25780
25797
  container.innerHTML = '<div class="empty-state" style="padding:16px">No toolkits available. Check that your Composio API key is valid.</div>';
25781
25798
  return;
25782
25799
  }
25800
+ // Stash full catalog in a closure so the search input can re-render
25801
+ // without refetching. Source of truth lives on the window object so
25802
+ // the input's oninput handler can find it.
25803
+ window._composioCatalog = {
25804
+ toolkits: toolkits,
25805
+ featured: new Set(d.featured || []),
25806
+ total: d.totalCount || toolkits.length,
25807
+ };
25808
+ renderComposioCatalog('');
25809
+ } catch (e) {
25810
+ container.innerHTML = '<div class="empty-state" style="color:var(--red);padding:16px">Failed to load Composio toolkits: ' + esc(String(e)) + '</div>';
25811
+ }
25812
+ }
25783
25813
 
25784
- var html = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px">';
25785
- toolkits.forEach(function(t) {
25786
- var connected = (t.connections || []).filter(function(c) { return c.status === 'ACTIVE'; });
25787
- var pending = (t.connections || []).filter(function(c) { return c.status !== 'ACTIVE'; });
25788
- var isConnected = connected.length > 0;
25789
- var statusColor = isConnected ? 'var(--green)' : (pending.length > 0 ? 'var(--yellow,#f59e0b)' : 'var(--text-muted)');
25790
- var byo = t.authMode === 'byo' && !t.hasAuthConfig;
25814
+ function renderComposioCatalog(query) {
25815
+ var container = document.getElementById('composio-connections');
25816
+ if (!container) return;
25817
+ var cat = window._composioCatalog;
25818
+ if (!cat) return;
25819
+ var q = (query || '').trim().toLowerCase();
25820
+ var visible;
25821
+ if (q.length === 0) {
25822
+ // Default view: featured set (connected toolkits + most-popular ones).
25823
+ visible = cat.toolkits.filter(function(t) { return cat.featured.has(t.slug); });
25824
+ } else {
25825
+ // Search across slug, name, and description. Cap to 80 to keep DOM
25826
+ // size reasonable when the query matches a generic word.
25827
+ visible = cat.toolkits.filter(function(t) {
25828
+ return t.slug.toLowerCase().indexOf(q) !== -1
25829
+ || (t.displayName||'').toLowerCase().indexOf(q) !== -1
25830
+ || (t.description||'').toLowerCase().indexOf(q) !== -1;
25831
+ }).slice(0, 80);
25832
+ }
25833
+ // Always show connected toolkits first within the visible set.
25834
+ visible.sort(function(a, b) {
25835
+ var aConn = (a.connections||[]).some(function(c){ return c.status==='ACTIVE'; }) ? 0 : 1;
25836
+ var bConn = (b.connections||[]).some(function(c){ return c.status==='ACTIVE'; }) ? 0 : 1;
25837
+ if (aConn !== bConn) return aConn - bConn;
25838
+ return (b.toolCount||0) - (a.toolCount||0);
25839
+ });
25791
25840
 
25792
- html += '<div style="border:1px solid var(--border);border-radius:8px;padding:12px;background:var(--bg-secondary);display:flex;flex-direction:column;gap:8px">';
25793
- html += '<div style="display:flex;align-items:center;gap:10px">';
25794
- if (t.logoUrl) {
25795
- html += '<img src="' + esc(t.logoUrl) + '" alt="" style="width:24px;height:24px;border-radius:4px;background:#fff;padding:2px;flex-shrink:0;object-fit:contain">';
25796
- } else {
25797
- html += '<div style="width:24px;height:24px;border-radius:4px;background:var(--bg-tertiary);flex-shrink:0"></div>';
25798
- }
25799
- html += '<div style="flex:1;min-width:0">';
25800
- html += '<div style="font-size:13px;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(t.displayName) + '</div>';
25801
- html += '<div style="font-size:10px;color:var(--text-muted)">' + (isConnected ? connected.length + ' account' + (connected.length !== 1 ? 's' : '') : 'Not connected') + '</div>';
25802
- html += '</div>';
25803
- html += '<span style="width:8px;height:8px;border-radius:50%;background:' + statusColor + ';flex-shrink:0"></span>';
25804
- html += '</div>';
25841
+ var header = '<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px">'
25842
+ + '<input type="search" id="composio-search" value="' + esc(q) + '" placeholder="Search ' + cat.total + ' toolkits — Gmail, Slack, Notion, …" oninput="renderComposioCatalog(this.value)"'
25843
+ + ' style="flex:1;padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px">'
25844
+ + '<span style="font-size:11px;color:var(--text-muted);white-space:nowrap">' + visible.length + (q.length === 0 ? ' featured' : ' match' + (visible.length !== 1 ? 'es' : '')) + ' / ' + cat.total + ' total</span>'
25845
+ + '</div>';
25805
25846
 
25806
- if (connected.length > 0) {
25807
- html += '<div style="display:flex;flex-direction:column;gap:4px">';
25808
- connected.forEach(function(c) {
25809
- var label = c.alias || c.accountLabel || c.accountEmail || c.accountName || 'connected';
25810
- html += '<div style="display:flex;align-items:center;gap:6px;font-size:11px">';
25811
- html += '<span style="flex:1;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(label) + '</span>';
25812
- html += '<button class="btn-sm" onclick="disconnectComposio(\\'' + esc(t.slug) + '\\',\\'' + esc(c.id) + '\\')" style="font-size:10px;padding:2px 6px;background:transparent;border:1px solid var(--border);color:var(--text-muted);border-radius:4px;cursor:pointer">Disconnect</button>';
25813
- html += '</div>';
25814
- });
25815
- html += '</div>';
25816
- }
25847
+ if (visible.length === 0) {
25848
+ container.innerHTML = header + '<div class="empty-state" style="padding:16px">No toolkits match "' + esc(q) + '". Try a different search.</div>';
25849
+ setTimeout(function() { var s = document.getElementById('composio-search'); if (s) { s.focus(); s.setSelectionRange(s.value.length, s.value.length); } }, 0);
25850
+ return;
25851
+ }
25817
25852
 
25818
- if (byo) {
25819
- html += '<div style="font-size:10px;color:var(--yellow,#f59e0b);background:rgba(245,158,11,0.08);padding:6px 8px;border-radius:4px;line-height:1.4">Needs a BYO OAuth app — <a href="https://platform.composio.dev/auth-configs" target="_blank" style="color:inherit;text-decoration:underline">set up in Composio</a> first.</div>';
25820
- }
25853
+ var html = header + '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px">';
25854
+ visible.forEach(function(t) {
25855
+ var connected = (t.connections || []).filter(function(c) { return c.status === 'ACTIVE'; });
25856
+ var pending = (t.connections || []).filter(function(c) { return c.status !== 'ACTIVE'; });
25857
+ var isConnected = connected.length > 0;
25858
+ var statusColor = isConnected ? 'var(--green)' : (pending.length > 0 ? 'var(--yellow,#f59e0b)' : 'var(--text-muted)');
25859
+ var byo = t.authMode === 'byo' && !t.hasAuthConfig;
25821
25860
 
25822
- html += '<button class="btn btn-sm btn-primary" onclick="connectComposio(\\'' + esc(t.slug) + '\\')" style="font-size:11px">' + (isConnected ? '+ Add another account' : 'Connect') + '</button>';
25861
+ html += '<div style="border:1px solid var(--border);border-radius:8px;padding:12px;background:var(--bg-secondary);display:flex;flex-direction:column;gap:8px">';
25862
+ html += '<div style="display:flex;align-items:center;gap:10px">';
25863
+ if (t.logoUrl) {
25864
+ html += '<img src="' + esc(t.logoUrl) + '" alt="" style="width:24px;height:24px;border-radius:4px;background:#fff;padding:2px;flex-shrink:0;object-fit:contain">';
25865
+ } else {
25866
+ html += '<div style="width:24px;height:24px;border-radius:4px;background:var(--bg-tertiary);flex-shrink:0"></div>';
25867
+ }
25868
+ html += '<div style="flex:1;min-width:0">';
25869
+ html += '<div style="font-size:13px;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(t.displayName) + '</div>';
25870
+ html += '<div style="font-size:10px;color:var(--text-muted)">' + (isConnected ? connected.length + ' account' + (connected.length !== 1 ? 's' : '') : ((t.toolCount||0) + ' tools')) + '</div>';
25871
+ html += '</div>';
25872
+ html += '<span style="width:8px;height:8px;border-radius:50%;background:' + statusColor + ';flex-shrink:0"></span>';
25873
+ html += '</div>';
25874
+
25875
+ if (connected.length > 0) {
25876
+ html += '<div style="display:flex;flex-direction:column;gap:4px">';
25877
+ connected.forEach(function(c) {
25878
+ var label = c.alias || c.accountLabel || c.accountEmail || c.accountName || 'connected';
25879
+ html += '<div style="display:flex;align-items:center;gap:6px;font-size:11px">';
25880
+ html += '<span style="flex:1;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(label) + '</span>';
25881
+ html += '<button class="btn-sm" onclick="disconnectComposio(\\'' + esc(t.slug) + '\\',\\'' + esc(c.id) + '\\')" style="font-size:10px;padding:2px 6px;background:transparent;border:1px solid var(--border);color:var(--text-muted);border-radius:4px;cursor:pointer">Disconnect</button>';
25882
+ html += '</div>';
25883
+ });
25823
25884
  html += '</div>';
25824
- });
25885
+ }
25886
+
25887
+ if (byo) {
25888
+ html += '<div style="font-size:10px;color:var(--yellow,#f59e0b);background:rgba(245,158,11,0.08);padding:6px 8px;border-radius:4px;line-height:1.4">Needs a BYO OAuth app — <a href="https://platform.composio.dev/auth-configs" target="_blank" style="color:inherit;text-decoration:underline">set up in Composio</a> first.</div>';
25889
+ }
25890
+
25891
+ html += '<button class="btn btn-sm btn-primary" onclick="connectComposio(\\'' + esc(t.slug) + '\\')" style="font-size:11px">' + (isConnected ? '+ Add account' : 'Connect') + '</button>';
25825
25892
  html += '</div>';
25826
- container.innerHTML = html;
25827
- } catch (e) {
25828
- container.innerHTML = '<div class="empty-state" style="color:var(--red);padding:16px">Failed to load Composio toolkits: ' + esc(String(e)) + '</div>';
25829
- }
25893
+ });
25894
+ html += '</div>';
25895
+ container.innerHTML = html;
25896
+ // Restore focus to the search input after re-render so users can keep typing.
25897
+ setTimeout(function() { var s = document.getElementById('composio-search'); if (s) { s.focus(); s.setSelectionRange(s.value.length, s.value.length); } }, 0);
25830
25898
  }
25831
25899
 
25832
25900
  async function saveComposioApiKey() {
@@ -54,6 +54,35 @@ export interface ToolkitMeta {
54
54
  description?: string;
55
55
  toolsCount?: number;
56
56
  }
57
+ /**
58
+ * Full catalog entry — derived directly from Composio's API. Replaces the
59
+ * hardcoded CURATED_TOOLKITS for UI rendering. The dashboard uses this so
60
+ * users can browse/search the entire catalog (1000+ services) instead of
61
+ * being limited to whatever slugs are pinned in code.
62
+ */
63
+ export interface CatalogToolkit {
64
+ slug: string;
65
+ name: string;
66
+ logoUrl?: string;
67
+ description?: string;
68
+ toolsCount?: number;
69
+ /** managed = Composio hosts the OAuth app; byo = user must register their
70
+ * own; none = no auth required. Derived from composioManagedAuthSchemes. */
71
+ authMode: 'managed' | 'byo' | 'none';
72
+ categories: {
73
+ slug: string;
74
+ name: string;
75
+ }[];
76
+ }
77
+ /**
78
+ * Fetch the full Composio toolkit catalog (1000+ services). Replaces the
79
+ * static CURATED_TOOLKITS array as the source of truth for the dashboard
80
+ * Connections panel — slug typos are now impossible because we render from
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.
84
+ */
85
+ export declare function listAllToolkits(): Promise<CatalogToolkit[]>;
57
86
  export declare function listToolkitMeta(): Promise<Map<string, ToolkitMeta>>;
58
87
  export declare function listToolkitSlugsWithAuthConfig(): Promise<Set<string>>;
59
88
  export declare class ComposioNeedsAuthConfigError extends Error {
@@ -82,6 +82,7 @@ export function resetComposioClient() {
82
82
  singleton = null;
83
83
  identityCache.clear();
84
84
  toolkitMetaCache = null;
85
+ catalogCache = null;
85
86
  detectedPreferredUserId = null;
86
87
  }
87
88
  // Public: same logic as the internal detector, exposed for the MCP bridge so
@@ -334,6 +335,64 @@ export async function listConnectedToolkits() {
334
335
  }
335
336
  }
336
337
  let toolkitMetaCache = null;
338
+ let catalogCache = null;
339
+ const CATALOG_TTL_MS = 60 * 60 * 1000; // 1h — catalog drifts very slowly
340
+ /**
341
+ * Fetch the full Composio toolkit catalog (1000+ services). Replaces the
342
+ * static CURATED_TOOLKITS array as the source of truth for the dashboard
343
+ * Connections panel — slug typos are now impossible because we render from
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.
347
+ */
348
+ export async function listAllToolkits() {
349
+ const now = Date.now();
350
+ if (catalogCache && now - catalogCache.at < CATALOG_TTL_MS) {
351
+ return catalogCache.data;
352
+ }
353
+ const composio = getComposio();
354
+ if (!composio)
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++) {
362
+ 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;
386
+ }
387
+ catch (err) {
388
+ logger.warn({ err, page }, 'listAllToolkits page failed — stopping pagination');
389
+ break;
390
+ }
391
+ }
392
+ catalogCache = { at: now, data: out };
393
+ logger.info({ count: out.length }, 'Composio catalog fetched');
394
+ return out;
395
+ }
337
396
  async function fetchAllToolkitMeta() {
338
397
  const composio = getComposio();
339
398
  if (!composio)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.11.3",
3
+ "version": "1.12.0",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",