clementine-agent 1.10.4 → 1.11.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.
|
@@ -61,7 +61,7 @@ export declare class ComposioNeedsAuthConfigError extends Error {
|
|
|
61
61
|
readonly underlying: string;
|
|
62
62
|
constructor(slug: string, underlying: string);
|
|
63
63
|
}
|
|
64
|
-
export declare function authorizeToolkit(slug: string,
|
|
64
|
+
export declare function authorizeToolkit(slug: string, _opts?: {
|
|
65
65
|
callbackUrl?: string;
|
|
66
66
|
alias?: string;
|
|
67
67
|
}): Promise<{
|
|
@@ -98,6 +98,10 @@ export async function getPreferredUserId() {
|
|
|
98
98
|
// calls have to pass *some* user_id, and we want it to match whatever the
|
|
99
99
|
// user already has if possible. detectPreferredUserId() picks the user_id
|
|
100
100
|
// with the most existing connections, falling back to this constant.
|
|
101
|
+
// Used only when there are no existing connections to learn from. The real
|
|
102
|
+
// path is detectPreferredUserId() reading the user_id off existing records
|
|
103
|
+
// via the raw client — that's how we match Composio's web-UI auto-generated
|
|
104
|
+
// IDs like `pg-test-<uuid>`.
|
|
101
105
|
const DEFAULT_NEW_CONNECTION_USER_ID = 'default';
|
|
102
106
|
export function clementineUserId() {
|
|
103
107
|
return readComposioEnv('COMPOSIO_USER_ID') || DEFAULT_NEW_CONNECTION_USER_ID;
|
|
@@ -108,26 +112,43 @@ async function detectPreferredUserId(composio) {
|
|
|
108
112
|
const explicit = readComposioEnv('COMPOSIO_USER_ID');
|
|
109
113
|
if (explicit)
|
|
110
114
|
return explicit;
|
|
111
|
-
if (detectedPreferredUserId)
|
|
115
|
+
if (detectedPreferredUserId !== null)
|
|
112
116
|
return detectedPreferredUserId;
|
|
117
|
+
// The high-level wrapper's list() drops the snake_case `user_id` field
|
|
118
|
+
// during its camelCase transformation, so connections look like they have
|
|
119
|
+
// no user_id. Use the raw client (snake_case shape) to actually read the
|
|
120
|
+
// user_id Composio's web UI assigned (typically `pg-test-<uuid>` for
|
|
121
|
+
// dashboard-created connections). Without this, we'd default to "default"
|
|
122
|
+
// and composio.create() / toolkits.authorize() would never see existing
|
|
123
|
+
// connections — every tool call would 401.
|
|
113
124
|
try {
|
|
114
|
-
const
|
|
125
|
+
const rawClient = composio.client;
|
|
126
|
+
const resp = await rawClient.connectedAccounts.list({});
|
|
127
|
+
const items = (resp?.items ?? []);
|
|
115
128
|
const counts = new Map();
|
|
116
|
-
for (const it of
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
for (const it of items) {
|
|
130
|
+
// Prefer ACTIVE connections — expired ones often outnumber active ones
|
|
131
|
+
// (3 outlooks: 1 ACTIVE + 2 EXPIRED in real data).
|
|
132
|
+
if (it.status === 'ACTIVE' && typeof it.user_id === 'string' && it.user_id.length > 0) {
|
|
133
|
+
counts.set(it.user_id, (counts.get(it.user_id) ?? 0) + 2); // weight active higher
|
|
134
|
+
}
|
|
135
|
+
else if (typeof it.user_id === 'string' && it.user_id.length > 0) {
|
|
136
|
+
counts.set(it.user_id, (counts.get(it.user_id) ?? 0) + 1);
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
139
|
if (counts.size > 0) {
|
|
123
140
|
const top = [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
141
|
+
logger.info({ userId: top, candidates: counts.size }, 'Detected Composio user_id from existing connections');
|
|
124
142
|
detectedPreferredUserId = top;
|
|
125
143
|
return top;
|
|
126
144
|
}
|
|
127
145
|
}
|
|
128
146
|
catch (err) {
|
|
129
|
-
logger.
|
|
147
|
+
logger.warn({ err }, 'Raw client user_id probe failed — using fallback');
|
|
130
148
|
}
|
|
149
|
+
// No existing connections. Fall back to "default" — composio.create()
|
|
150
|
+
// requires a non-empty string, and "default" is the conventional
|
|
151
|
+
// single-tenant value that Composio's quickstart uses.
|
|
131
152
|
detectedPreferredUserId = DEFAULT_NEW_CONNECTION_USER_ID;
|
|
132
153
|
return DEFAULT_NEW_CONNECTION_USER_ID;
|
|
133
154
|
}
|
|
@@ -388,66 +409,34 @@ export class ComposioNeedsAuthConfigError extends Error {
|
|
|
388
409
|
this.name = 'ComposioNeedsAuthConfigError';
|
|
389
410
|
}
|
|
390
411
|
}
|
|
391
|
-
export async function authorizeToolkit(slug,
|
|
412
|
+
export async function authorizeToolkit(slug,
|
|
413
|
+
// Reserved for future per-call overrides (callbackUrl / alias). Currently
|
|
414
|
+
// unused because composio.toolkits.authorize doesn't surface those options
|
|
415
|
+
// — alias is set later via patch, callback uses Composio's hosted page.
|
|
416
|
+
_opts) {
|
|
392
417
|
const composio = getComposio();
|
|
393
418
|
if (!composio)
|
|
394
419
|
throw new Error('COMPOSIO_API_KEY not set');
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
catch (err) {
|
|
403
|
-
logger.error({ err, slug, step: 'authConfigs.list' }, 'Composio authorize failed');
|
|
404
|
-
throw err;
|
|
405
|
-
}
|
|
406
|
-
if (existingConfig) {
|
|
407
|
-
authConfigId = existingConfig.id;
|
|
408
|
-
logger.debug({ slug, authConfigId }, 'Reusing existing auth config');
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
try {
|
|
412
|
-
const created = await composio.authConfigs.create(slug, {
|
|
413
|
-
type: 'use_composio_managed_auth',
|
|
414
|
-
name: `${displayNameFor(slug)} Auth Config`,
|
|
415
|
-
});
|
|
416
|
-
authConfigId = created.id;
|
|
417
|
-
logger.debug({ slug, authConfigId }, 'Created managed auth config');
|
|
418
|
-
}
|
|
419
|
-
catch (err) {
|
|
420
|
-
const status = err?.status;
|
|
421
|
-
logger.warn({ err, slug, status, step: 'authConfigs.create' }, 'authConfigs.create rejected — likely needs BYO setup');
|
|
422
|
-
// 400 → Composio doesn't host a managed OAuth app for this toolkit.
|
|
423
|
-
// 401/403 → key lacks permission to create managed auth configs (common
|
|
424
|
-
// for Google services where Composio's plan tier requires you to
|
|
425
|
-
// register your own Google OAuth project). Either way, the user fix
|
|
426
|
-
// is the same: set it up once in Composio's auth-configs dashboard.
|
|
427
|
-
if (status === 400 || status === 401 || status === 403) {
|
|
428
|
-
throw new ComposioNeedsAuthConfigError(slug, String(err));
|
|
429
|
-
}
|
|
430
|
-
throw err;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
// 2. Initiate the connection. allowMultiple if there's already an active
|
|
434
|
-
// connection so we add another account instead of replacing.
|
|
435
|
-
const existing = (await listConnectedToolkits()).filter(c => c.slug === slug && c.status === 'ACTIVE');
|
|
436
|
-
// Reuse whichever user_id already owns connections in this account, so a
|
|
437
|
-
// freshly authorized Gmail lands next to the existing Outlook (etc.) under
|
|
438
|
-
// the same user_id. Falls back to env override or "default".
|
|
420
|
+
// Use the SDK's blessed one-liner — handles auth config discovery
|
|
421
|
+
// (reuses existing managed config if present) AND new-config creation
|
|
422
|
+
// automatically. Source: README + composio-B75SFMkx.d.cts. Replaces our
|
|
423
|
+
// older two-step `authConfigs.list/create + connectedAccounts.initiate`
|
|
424
|
+
// flow which tripped 401s on plans that don't allow programmatic
|
|
425
|
+
// authConfigs.create even when a managed app exists.
|
|
439
426
|
const userId = await detectPreferredUserId(composio);
|
|
440
427
|
try {
|
|
441
|
-
const conn = await composio.
|
|
442
|
-
|
|
443
|
-
...(opts?.callbackUrl ? { callbackUrl: opts.callbackUrl } : {}),
|
|
444
|
-
...(opts?.alias ? { alias: opts.alias } : {}),
|
|
445
|
-
});
|
|
446
|
-
logger.debug({ slug, userId, connectionId: conn.id }, 'Initiated connection');
|
|
428
|
+
const conn = await composio.toolkits.authorize(userId, slug);
|
|
429
|
+
logger.info({ slug, userId, connectionId: conn.id }, 'Composio authorize OK');
|
|
447
430
|
return { redirectUrl: conn.redirectUrl ?? null, connectionId: conn.id };
|
|
448
431
|
}
|
|
449
432
|
catch (err) {
|
|
450
|
-
|
|
433
|
+
const status = err?.status;
|
|
434
|
+
logger.error({ err, slug, userId, status, step: 'toolkits.authorize' }, 'Composio authorize failed');
|
|
435
|
+
// Translate the documented "no managed auth available" error codes into
|
|
436
|
+
// the friendly BYO-setup banner the dashboard already renders.
|
|
437
|
+
if (status === 400 || status === 401 || status === 403) {
|
|
438
|
+
throw new ComposioNeedsAuthConfigError(slug, String(err));
|
|
439
|
+
}
|
|
451
440
|
throw err;
|
|
452
441
|
}
|
|
453
442
|
}
|
|
@@ -48,29 +48,23 @@ export async function buildComposioMcpServers(slugs) {
|
|
|
48
48
|
}
|
|
49
49
|
return out;
|
|
50
50
|
}
|
|
51
|
-
async function buildOne(composio, slug,
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
const authConfig = (await composio.authConfigs.list({ toolkit: slug })).items[0];
|
|
51
|
+
async function buildOne(composio, slug, _connected) {
|
|
52
|
+
// composio.tools.get() returns the FLAT toolkit tools (OUTLOOK_LIST_MESSAGES,
|
|
53
|
+
// GMAIL_SEND_EMAIL, …) — exactly the namespacing the agent expects as
|
|
54
|
+
// mcp__outlook__OUTLOOK_LIST_MESSAGES. The alternative, composio.create()
|
|
55
|
+
// + session.tools(), uses Composio's tool-router pattern and only returns
|
|
56
|
+
// 5 meta-tools (COMPOSIO_SEARCH_TOOLS, COMPOSIO_MULTI_EXECUTE_TOOL, …),
|
|
57
|
+
// which doesn't match what the agent calls. Verified empirically:
|
|
58
|
+
// tools.get returns 50+ actual Outlook tools.
|
|
60
59
|
const userId = await getPreferredUserId();
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
...(activeCount >= 2
|
|
66
|
-
? { multiAccount: { enable: true, requireExplicitSelection: true } }
|
|
67
|
-
: {}),
|
|
68
|
-
});
|
|
69
|
-
const tools = await session.tools();
|
|
60
|
+
const toolsRaw = await composio.tools.get(userId, { toolkits: [slug], limit: 200 });
|
|
61
|
+
// tools.get can return an array OR an object depending on provider; normalise.
|
|
62
|
+
const toolsArr = Array.isArray(toolsRaw) ? toolsRaw : Object.values(toolsRaw);
|
|
63
|
+
const tools = toolsArr.filter((t) => t && typeof t.name === 'string' && typeof t.handler === 'function');
|
|
70
64
|
return createSdkMcpServer({
|
|
71
65
|
name: slug,
|
|
72
66
|
version: '0.1.0',
|
|
73
|
-
tools,
|
|
67
|
+
tools: tools,
|
|
74
68
|
});
|
|
75
69
|
}
|
|
76
70
|
//# sourceMappingURL=mcp-bridge.js.map
|