clementine-agent 1.11.0 → 1.11.2

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.
@@ -25849,9 +25849,10 @@ async function saveComposioApiKey() {
25849
25849
 
25850
25850
  async function connectComposio(slug) {
25851
25851
  try {
25852
- var res = await fetch('/api/composio/toolkits/' + encodeURIComponent(slug) + '/authorize', {
25852
+ // Use apiFetch (not raw fetch) so the Authorization: Bearer header is
25853
+ // attached — the /api/* middleware rejects unauth'd POSTs with 401.
25854
+ var res = await apiFetch('/api/composio/toolkits/' + encodeURIComponent(slug) + '/authorize', {
25853
25855
  method: 'POST',
25854
- credentials: 'same-origin',
25855
25856
  headers: { 'content-type': 'application/json' },
25856
25857
  body: JSON.stringify({}),
25857
25858
  });
@@ -25884,9 +25885,8 @@ async function connectComposio(slug) {
25884
25885
  async function disconnectComposio(slug, connectionId) {
25885
25886
  if (!confirm('Disconnect this ' + slug + ' account?')) return;
25886
25887
  try {
25887
- var res = await fetch('/api/composio/toolkits/' + encodeURIComponent(slug) + '/disconnect', {
25888
+ var res = await apiFetch('/api/composio/toolkits/' + encodeURIComponent(slug) + '/disconnect', {
25888
25889
  method: 'POST',
25889
- credentials: 'same-origin',
25890
25890
  headers: { 'content-type': 'application/json' },
25891
25891
  body: JSON.stringify({ connectionId: connectionId }),
25892
25892
  });
@@ -98,11 +98,11 @@ 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
- // Empty string matches Composio's web-UI default user_id (verified by
102
- // probing connections created via dashboard.composio.dev land under '').
103
- // Using the same value here keeps Clementine-authorized connections in the
104
- // same bucket as anything the user already has from the web UI.
105
- const DEFAULT_NEW_CONNECTION_USER_ID = '';
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>`.
105
+ const DEFAULT_NEW_CONNECTION_USER_ID = 'default';
106
106
  export function clementineUserId() {
107
107
  return readComposioEnv('COMPOSIO_USER_ID') || DEFAULT_NEW_CONNECTION_USER_ID;
108
108
  }
@@ -114,35 +114,41 @@ async function detectPreferredUserId(composio) {
114
114
  return explicit;
115
115
  if (detectedPreferredUserId !== null)
116
116
  return detectedPreferredUserId;
117
- // The list response doesn't expose userId on records (verified empirically),
118
- // so we can't read it from the data. Instead, probe each candidate user_id
119
- // with a filtered list and pick whichever returns connections. This matches
120
- // however the connections were originally created including empty string,
121
- // which is what Composio's web UI uses by default for new accounts.
122
- const candidates = ['', 'default', 'user', 'me'];
123
- let totalUnscoped = 0;
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.
124
124
  try {
125
- const all = await composio.connectedAccounts.list({ limit: 1 });
126
- totalUnscoped = all.items.length;
127
- }
128
- catch { /* non-fatal */ }
129
- if (totalUnscoped > 0) {
130
- for (const uid of candidates) {
131
- try {
132
- const resp = await composio.connectedAccounts.list({ userIds: [uid], limit: 1 });
133
- if (resp.items.length > 0) {
134
- logger.info({ userId: uid }, 'Detected Composio user_id via probe');
135
- detectedPreferredUserId = uid;
136
- return uid;
137
- }
125
+ const rawClient = composio.client;
126
+ const resp = await rawClient.connectedAccounts.list({});
127
+ const items = (resp?.items ?? []);
128
+ const counts = new Map();
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);
138
137
  }
139
- catch { /* try next */ }
140
138
  }
139
+ if (counts.size > 0) {
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');
142
+ detectedPreferredUserId = top;
143
+ return top;
144
+ }
145
+ }
146
+ catch (err) {
147
+ logger.warn({ err }, 'Raw client user_id probe failed — using fallback');
141
148
  }
142
- // No connections yet, or none of the common user_ids match. Fall back to
143
- // empty string, which is what Composio's web UI uses by default — keeps
144
- // future Clementine-authorized connections in the same bucket as anything
145
- // the user creates in Composio's dashboard.
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.
146
152
  detectedPreferredUserId = DEFAULT_NEW_CONNECTION_USER_ID;
147
153
  return DEFAULT_NEW_CONNECTION_USER_ID;
148
154
  }
@@ -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.11.0",
3
+ "version": "1.11.2",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",