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, opts?: {
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 resp = await composio.connectedAccounts.list({ limit: 100 });
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 resp.items) {
117
- const uid = it.userId ?? it.user_id;
118
- if (typeof uid === 'string' && uid.length > 0) {
119
- counts.set(uid, (counts.get(uid) ?? 0) + 1);
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.debug({ err }, 'detectPreferredUserId failed — using default');
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, opts) {
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
- // 1. Find or create an auth config. session.authorize() doesn't auto-create
396
- // so we have to pass authConfigId explicitly to connectedAccounts.initiate.
397
- let authConfigId;
398
- let existingConfig;
399
- try {
400
- existingConfig = (await composio.authConfigs.list({ toolkit: slug })).items[0];
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.connectedAccounts.initiate(userId, authConfigId, {
442
- ...(existing.length > 0 ? { allowMultiple: true } : {}),
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
- logger.error({ err, slug, userId, authConfigId, step: 'connectedAccounts.initiate' }, 'Composio initiate failed');
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, connected) {
52
- // If 2+ active connections for this toolkit, force Composio to require
53
- // explicit account selection per tool call otherwise it silently picks
54
- // the default (newest) account.
55
- const activeCount = connected.filter(c => c.slug === slug && c.status === 'ACTIVE').length;
56
- // Look up auth config explicitly. Without this, composio.create() tries to
57
- // auto-create one and 400s for BYO toolkits (Twitter etc.) that don't have
58
- // a managed OAuth app available.
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 session = await composio.create(userId, {
62
- toolkits: [slug],
63
- manageConnections: false,
64
- ...(authConfig ? { authConfigs: { [slug]: authConfig.id } } : {}),
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.10.4",
3
+ "version": "1.11.1",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",