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.
package/dist/cli/dashboard.js
CHANGED
|
@@ -4478,7 +4478,19 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4478
4478
|
});
|
|
4479
4479
|
return;
|
|
4480
4480
|
}
|
|
4481
|
-
|
|
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) {
|
|
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
|
-
//
|
|
73
|
-
//
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 } } : {}),
|