clementine-agent 1.10.0 → 1.10.1

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.
@@ -4478,7 +4478,19 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
4478
4478
  });
4479
4479
  return;
4480
4480
  }
4481
- res.status(500).json({ error: String(err) });
4481
+ // Composio errors carry useful detail in `.error`, `.message`, and
4482
+ // `.status`; serializing with String() drops everything except the name.
4483
+ // Pull whatever's there and log server-side so we have a paper trail.
4484
+ const e = err;
4485
+ const detail = e?.error
4486
+ ? (typeof e.error === 'string' ? e.error : (e.error.message ?? JSON.stringify(e.error)))
4487
+ : (e?.message ?? String(err));
4488
+ console.error(`[composio] authorize ${slug} failed:`, detail, e?.stack ?? '');
4489
+ res.status(500).json({
4490
+ error: detail,
4491
+ toolkit: slug,
4492
+ statusCode: e?.status,
4493
+ });
4482
4494
  }
4483
4495
  });
4484
4496
  app.post('/api/composio/toolkits/:slug/disconnect', async (req, res) => {
@@ -25839,7 +25851,12 @@ async function connectComposio(slug) {
25839
25851
  window.open(d.setupUrl, '_blank');
25840
25852
  return;
25841
25853
  }
25842
- if (!res.ok) { toast('Connect failed: ' + (d.error || res.status), 'error'); return; }
25854
+ if (!res.ok) {
25855
+ var reason = d.error || ('HTTP ' + res.status);
25856
+ toast('Connect failed: ' + reason, 'error');
25857
+ console.error('[composio] connect failed', { slug: slug, status: res.status, body: d });
25858
+ return;
25859
+ }
25843
25860
  if (d.redirectUrl) {
25844
25861
  window.open(d.redirectUrl, '_blank');
25845
25862
  toast('Opened ' + slug + ' authorization in a new tab. Refresh after approving.', 'info');
@@ -32,6 +32,7 @@ export declare function isComposioEnabled(): boolean;
32
32
  * the dashboard PUT /api/settings/COMPOSIO_API_KEY handler.
33
33
  */
34
34
  export declare function resetComposioClient(): void;
35
+ export declare function getPreferredUserId(): Promise<string>;
35
36
  export declare function clementineUserId(): string;
36
37
  export declare function displayNameFor(slug: string): string;
37
38
  export interface ConnectedToolkit {
@@ -40,6 +40,9 @@ export const CURATED_TOOLKITS = [
40
40
  { slug: 'stripe', displayName: 'Stripe', authMode: 'managed' },
41
41
  { slug: 'supabase', displayName: 'Supabase', authMode: 'managed' },
42
42
  { slug: 'linkedin', displayName: 'LinkedIn', authMode: 'managed' },
43
+ { slug: 'outlook', displayName: 'Outlook', authMode: 'managed' },
44
+ { slug: 'onedrive', displayName: 'OneDrive', authMode: 'managed' },
45
+ { slug: 'zoom', displayName: 'Zoom', authMode: 'managed' },
43
46
  { slug: 'twitter', displayName: 'Twitter / X', authMode: 'byo' },
44
47
  ];
45
48
  const DISPLAY_NAME_BY_SLUG = new Map(CURATED_TOOLKITS.map(t => [t.slug, t.displayName]));
@@ -68,11 +71,53 @@ export function resetComposioClient() {
68
71
  singleton = null;
69
72
  identityCache.clear();
70
73
  toolkitMetaCache = null;
74
+ detectedPreferredUserId = null;
71
75
  }
72
- // Single-tenant by default. All connections are keyed under COMPOSIO_USER_ID;
73
- // override if the same Composio account is shared with another tool.
76
+ // Public: same logic as the internal detector, exposed for the MCP bridge so
77
+ // agent sessions land on the right user_id.
78
+ export async function getPreferredUserId() {
79
+ const composio = getComposio();
80
+ if (!composio)
81
+ return clementineUserId();
82
+ return detectPreferredUserId(composio);
83
+ }
84
+ // Default user_id for *new* connections. We list connections without filtering
85
+ // so existing accounts (set up in Composio's web UI under the platform default
86
+ // "default" user_id, or any other label) still surface — but new authorize()
87
+ // calls have to pass *some* user_id, and we want it to match whatever the
88
+ // user already has if possible. detectPreferredUserId() picks the user_id
89
+ // with the most existing connections, falling back to this constant.
90
+ const DEFAULT_NEW_CONNECTION_USER_ID = 'default';
74
91
  export function clementineUserId() {
75
- return process.env.COMPOSIO_USER_ID ?? 'clementine-default';
92
+ return process.env.COMPOSIO_USER_ID ?? DEFAULT_NEW_CONNECTION_USER_ID;
93
+ }
94
+ // Cached after first detection — avoids extra API calls per authorize.
95
+ let detectedPreferredUserId = null;
96
+ async function detectPreferredUserId(composio) {
97
+ if (process.env.COMPOSIO_USER_ID)
98
+ return process.env.COMPOSIO_USER_ID;
99
+ if (detectedPreferredUserId)
100
+ return detectedPreferredUserId;
101
+ try {
102
+ const resp = await composio.connectedAccounts.list({ limit: 100 });
103
+ const counts = new Map();
104
+ for (const it of resp.items) {
105
+ const uid = it.userId ?? it.user_id;
106
+ if (typeof uid === 'string' && uid.length > 0) {
107
+ counts.set(uid, (counts.get(uid) ?? 0) + 1);
108
+ }
109
+ }
110
+ if (counts.size > 0) {
111
+ const top = [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
112
+ detectedPreferredUserId = top;
113
+ return top;
114
+ }
115
+ }
116
+ catch (err) {
117
+ logger.debug({ err }, 'detectPreferredUserId failed — using default');
118
+ }
119
+ detectedPreferredUserId = DEFAULT_NEW_CONNECTION_USER_ID;
120
+ return DEFAULT_NEW_CONNECTION_USER_ID;
76
121
  }
77
122
  export function displayNameFor(slug) {
78
123
  return DISPLAY_NAME_BY_SLUG.get(slug) ?? humanize(slug);
@@ -226,7 +271,11 @@ export async function listConnectedToolkits() {
226
271
  if (!composio)
227
272
  return [];
228
273
  try {
229
- const resp = await composio.connectedAccounts.list({ userIds: [clementineUserId()] });
274
+ // No userIds filter: a Composio API key is account-scoped, and a personal
275
+ // agent should see every connection on the account regardless of which
276
+ // user_id label it was created under. This is the fix for "I connected X
277
+ // in Composio but it doesn't show up in Clementine."
278
+ const resp = await composio.connectedAccounts.list({ limit: 100 });
230
279
  const enriched = await Promise.all(resp.items.map(async (it) => {
231
280
  const seed = extractAccountIdentity(it.state, it.data);
232
281
  const identity = it.status === 'ACTIVE'
@@ -359,7 +408,11 @@ export async function authorizeToolkit(slug, opts) {
359
408
  // 2. Initiate the connection. allowMultiple if there's already an active
360
409
  // connection so we add another account instead of replacing.
361
410
  const existing = (await listConnectedToolkits()).filter(c => c.slug === slug && c.status === 'ACTIVE');
362
- const conn = await composio.connectedAccounts.initiate(clementineUserId(), authConfigId, {
411
+ // Reuse whichever user_id already owns connections in this account, so a
412
+ // freshly authorized Gmail lands next to the existing Outlook (etc.) under
413
+ // the same user_id. Falls back to env override or "default".
414
+ const userId = await detectPreferredUserId(composio);
415
+ const conn = await composio.connectedAccounts.initiate(userId, authConfigId, {
363
416
  ...(existing.length > 0 ? { allowMultiple: true } : {}),
364
417
  ...(opts?.callbackUrl ? { callbackUrl: opts.callbackUrl } : {}),
365
418
  ...(opts?.alias ? { alias: opts.alias } : {}),
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
19
19
  import pino from 'pino';
20
- import { clementineUserId, getComposio, listConnectedToolkits, } from './client.js';
20
+ import { getComposio, getPreferredUserId, listConnectedToolkits, } from './client.js';
21
21
  const logger = pino({ name: 'clementine.composio.mcp' });
22
22
  /**
23
23
  * Build SDK MCP server configs for the given toolkit slugs (or all active
@@ -57,7 +57,8 @@ async function buildOne(composio, slug, connected) {
57
57
  // auto-create one and 400s for BYO toolkits (Twitter etc.) that don't have
58
58
  // a managed OAuth app available.
59
59
  const authConfig = (await composio.authConfigs.list({ toolkit: slug })).items[0];
60
- const session = await composio.create(clementineUserId(), {
60
+ const userId = await getPreferredUserId();
61
+ const session = await composio.create(userId, {
61
62
  toolkits: [slug],
62
63
  manageConnections: false,
63
64
  ...(authConfig ? { authConfigs: { [slug]: authConfig.id } } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",