clementine-agent 1.9.1 → 1.10.0
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/agent/assistant.js +24 -7
- package/dist/cli/dashboard.js +301 -2
- package/dist/cli/setup.js +13 -0
- package/dist/config/integrations-registry.js +13 -0
- package/dist/integrations/composio/client.d.ts +72 -0
- package/dist/integrations/composio/client.js +387 -0
- package/dist/integrations/composio/mcp-bridge.d.ts +24 -0
- package/dist/integrations/composio/mcp-bridge.js +75 -0
- package/package.json +3 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -1639,7 +1639,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1639
1639
|
};
|
|
1640
1640
|
}
|
|
1641
1641
|
// ── Build SDK Options ─────────────────────────────────────────────
|
|
1642
|
-
buildOptions(opts = {}) {
|
|
1642
|
+
async buildOptions(opts = {}) {
|
|
1643
1643
|
const { isHeartbeat = false, cronTier = null, maxTurns = null, model = null, enableTeams = true, retrievalContext = '', profile = null, sessionKey = null, streaming = false, isPlanStep = false, isUnleashed = false, sourceOverride, disableAllTools = false, verboseLevel, abortController, effort, maxBudgetUsd, thinking, outputFormat, stallGuard, intentClassification, } = opts;
|
|
1644
1644
|
let allowedTools = [
|
|
1645
1645
|
'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep',
|
|
@@ -1928,6 +1928,22 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1928
1928
|
}
|
|
1929
1929
|
}
|
|
1930
1930
|
catch { /* non-fatal — run with just Clementine's own server */ }
|
|
1931
|
+
// Composio toolkits — each connected toolkit becomes an in-process MCP
|
|
1932
|
+
// server (mcp__gmail__*, mcp__slack__*, …). Profile-level allowlist
|
|
1933
|
+
// (profile.allowedComposioToolkits) constrains which toolkits this agent
|
|
1934
|
+
// sees; omit it to surface every active connection.
|
|
1935
|
+
let composioMcpServers = {};
|
|
1936
|
+
if (!disableAllTools && !isPlanStep) {
|
|
1937
|
+
try {
|
|
1938
|
+
const { buildComposioMcpServers } = await import('../integrations/composio/mcp-bridge.js');
|
|
1939
|
+
const allowList = profile?.allowedComposioToolkits;
|
|
1940
|
+
composioMcpServers = await buildComposioMcpServers(allowList);
|
|
1941
|
+
}
|
|
1942
|
+
catch (err) {
|
|
1943
|
+
// Composio is purely additive — never block the agent if it fails.
|
|
1944
|
+
logger.debug({ err }, 'Composio MCP servers unavailable');
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1931
1947
|
// Permission mode: always 'bypassPermissions' — this is a daemon/harness with no interactive
|
|
1932
1948
|
// terminal, so 'auto' mode (which requires plan support + human approval) doesn't apply.
|
|
1933
1949
|
const effectivePermissionMode = 'bypassPermissions';
|
|
@@ -1973,6 +1989,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1973
1989
|
},
|
|
1974
1990
|
},
|
|
1975
1991
|
...externalMcpServers,
|
|
1992
|
+
...composioMcpServers,
|
|
1976
1993
|
},
|
|
1977
1994
|
...(abortController ? { abortController } : {}),
|
|
1978
1995
|
maxTurns: effectiveMaxTurns,
|
|
@@ -2602,7 +2619,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2602
2619
|
let contradictionRetried = false;
|
|
2603
2620
|
try {
|
|
2604
2621
|
for (let attempt = 0; attempt <= PersonalAssistant.RATE_LIMIT_MAX_RETRIES; attempt++) {
|
|
2605
|
-
const sdkOptions = this.buildOptions({ model, maxTurns: maxTurnsOverride ?? null, retrievalContext, profile, sessionKey, streaming: !!onText, verboseLevel, abortController, stallGuard, intentClassification, effort: intentClassification?.suggestedEffort });
|
|
2622
|
+
const sdkOptions = await this.buildOptions({ model, maxTurns: maxTurnsOverride ?? null, retrievalContext, profile, sessionKey, streaming: !!onText, verboseLevel, abortController, stallGuard, intentClassification, effort: intentClassification?.suggestedEffort });
|
|
2606
2623
|
// If a project matched, switch cwd so the agent gets its tools/CLAUDE.md
|
|
2607
2624
|
if (matchedProject) {
|
|
2608
2625
|
sdkOptions.cwd = matchedProject.path;
|
|
@@ -3851,7 +3868,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3851
3868
|
// ── Heartbeat / Cron ──────────────────────────────────────────────
|
|
3852
3869
|
async heartbeat(standingInstructions, changesSummary = '', timeContext = '', dedupContext = '', profile) {
|
|
3853
3870
|
setInteractionSource('autonomous');
|
|
3854
|
-
const sdkOptions = this.buildOptions({
|
|
3871
|
+
const sdkOptions = await this.buildOptions({
|
|
3855
3872
|
isHeartbeat: true,
|
|
3856
3873
|
enableTeams: false,
|
|
3857
3874
|
model: MODELS.haiku,
|
|
@@ -3921,7 +3938,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3921
3938
|
// Don't mutate the global — pass source through the closure instead
|
|
3922
3939
|
// Per-step stall guard so concurrent steps don't cross-contaminate
|
|
3923
3940
|
const stepGuard = new StallGuard();
|
|
3924
|
-
const sdkOptions = this.buildOptions({
|
|
3941
|
+
const sdkOptions = await this.buildOptions({
|
|
3925
3942
|
isHeartbeat: false,
|
|
3926
3943
|
cronTier: tier,
|
|
3927
3944
|
maxTurns,
|
|
@@ -3986,7 +4003,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3986
4003
|
// not chat text — pass mode='cron' so high_effort_low_output guard is
|
|
3987
4004
|
// disabled. Loop detection and circular-reasoning checks stay active.
|
|
3988
4005
|
const cronGuard = new StallGuard('cron');
|
|
3989
|
-
const sdkOptions = this.buildOptions({
|
|
4006
|
+
const sdkOptions = await this.buildOptions({
|
|
3990
4007
|
isHeartbeat: true,
|
|
3991
4008
|
cronTier: tier,
|
|
3992
4009
|
maxTurns: maxTurns ?? (tier >= 2 ? 30 : 15),
|
|
@@ -4473,7 +4490,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4473
4490
|
_setActiveQueryContext({ job: jobName, source: 'unleashed', agentSlug });
|
|
4474
4491
|
// Unleashed phases run side-effect-heavy work; same logic as cron mode.
|
|
4475
4492
|
const phaseGuard = new StallGuard('unleashed');
|
|
4476
|
-
const sdkOptions = this.buildOptions({
|
|
4493
|
+
const sdkOptions = await this.buildOptions({
|
|
4477
4494
|
isHeartbeat: true,
|
|
4478
4495
|
cronTier: tier,
|
|
4479
4496
|
maxTurns: turnsPerPhase,
|
|
@@ -4830,7 +4847,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4830
4847
|
logger.info({ taskName, phase }, `Team task: starting phase ${phase}`);
|
|
4831
4848
|
setInteractionSource('autonomous');
|
|
4832
4849
|
const teamGuard = new StallGuard();
|
|
4833
|
-
const sdkOptions = this.buildOptions({
|
|
4850
|
+
const sdkOptions = await this.buildOptions({
|
|
4834
4851
|
isHeartbeat: true,
|
|
4835
4852
|
cronTier: 2, // Give full tool access (Bash, Write, Edit)
|
|
4836
4853
|
maxTurns: turnsPerPhase,
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -4386,6 +4386,132 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4386
4386
|
res.status(500).json({ error: String(err) });
|
|
4387
4387
|
}
|
|
4388
4388
|
});
|
|
4389
|
+
// ── Composio (1000+ third-party services via OAuth broker) ────
|
|
4390
|
+
app.get('/api/composio/status', (_req, res) => {
|
|
4391
|
+
res.json({ enabled: Boolean(process.env.COMPOSIO_API_KEY) });
|
|
4392
|
+
});
|
|
4393
|
+
app.get('/api/composio/toolkits', async (_req, res) => {
|
|
4394
|
+
try {
|
|
4395
|
+
if (!process.env.COMPOSIO_API_KEY) {
|
|
4396
|
+
res.json({ enabled: false, toolkits: [] });
|
|
4397
|
+
return;
|
|
4398
|
+
}
|
|
4399
|
+
const c = await import('../integrations/composio/client.js');
|
|
4400
|
+
const [connected, configured, meta] = await Promise.all([
|
|
4401
|
+
c.listConnectedToolkits(),
|
|
4402
|
+
c.listToolkitSlugsWithAuthConfig(),
|
|
4403
|
+
c.listToolkitMeta(),
|
|
4404
|
+
]);
|
|
4405
|
+
const connectionsBySlug = new Map();
|
|
4406
|
+
for (const conn of connected) {
|
|
4407
|
+
const arr = connectionsBySlug.get(conn.slug) ?? [];
|
|
4408
|
+
arr.push(conn);
|
|
4409
|
+
connectionsBySlug.set(conn.slug, arr);
|
|
4410
|
+
}
|
|
4411
|
+
for (const arr of connectionsBySlug.values()) {
|
|
4412
|
+
arr.sort((a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? ''));
|
|
4413
|
+
}
|
|
4414
|
+
const toView = (conn) => ({
|
|
4415
|
+
id: conn.connectionId,
|
|
4416
|
+
status: conn.status,
|
|
4417
|
+
alias: conn.alias ?? null,
|
|
4418
|
+
accountLabel: conn.accountLabel ?? null,
|
|
4419
|
+
accountEmail: conn.accountEmail ?? null,
|
|
4420
|
+
accountName: conn.accountName ?? null,
|
|
4421
|
+
accountAvatarUrl: conn.accountAvatarUrl ?? null,
|
|
4422
|
+
createdAt: conn.createdAt ?? null,
|
|
4423
|
+
});
|
|
4424
|
+
const curated = c.CURATED_TOOLKITS.map(t => {
|
|
4425
|
+
const m = meta.get(t.slug);
|
|
4426
|
+
const conns = connectionsBySlug.get(t.slug) ?? [];
|
|
4427
|
+
return {
|
|
4428
|
+
slug: t.slug,
|
|
4429
|
+
displayName: t.displayName,
|
|
4430
|
+
authMode: t.authMode,
|
|
4431
|
+
hasAuthConfig: configured.has(t.slug),
|
|
4432
|
+
logoUrl: m?.logo ?? null,
|
|
4433
|
+
description: m?.description ?? null,
|
|
4434
|
+
toolCount: m?.toolsCount ?? null,
|
|
4435
|
+
connections: conns.map(toView),
|
|
4436
|
+
};
|
|
4437
|
+
});
|
|
4438
|
+
const extras = [...connectionsBySlug.entries()]
|
|
4439
|
+
.filter(([slug]) => !c.CURATED_TOOLKITS.some(t => t.slug === slug))
|
|
4440
|
+
.map(([slug, conns]) => {
|
|
4441
|
+
const m = meta.get(slug);
|
|
4442
|
+
// Non-curated: infer auth mode. If a custom auth config exists,
|
|
4443
|
+
// user set it up themselves (BYO). Otherwise it must be managed.
|
|
4444
|
+
const authMode = configured.has(slug) ? 'byo' : 'managed';
|
|
4445
|
+
return {
|
|
4446
|
+
slug,
|
|
4447
|
+
displayName: m?.name ?? c.displayNameFor(slug),
|
|
4448
|
+
authMode,
|
|
4449
|
+
hasAuthConfig: configured.has(slug),
|
|
4450
|
+
logoUrl: m?.logo ?? null,
|
|
4451
|
+
description: m?.description ?? null,
|
|
4452
|
+
toolCount: m?.toolsCount ?? null,
|
|
4453
|
+
connections: conns.map(toView),
|
|
4454
|
+
};
|
|
4455
|
+
});
|
|
4456
|
+
res.json({ enabled: true, toolkits: [...curated, ...extras] });
|
|
4457
|
+
}
|
|
4458
|
+
catch (err) {
|
|
4459
|
+
res.status(500).json({ error: String(err) });
|
|
4460
|
+
}
|
|
4461
|
+
});
|
|
4462
|
+
app.post('/api/composio/toolkits/:slug/authorize', async (req, res) => {
|
|
4463
|
+
const slug = req.params.slug;
|
|
4464
|
+
const alias = typeof req.body?.alias === 'string' ? req.body.alias : undefined;
|
|
4465
|
+
try {
|
|
4466
|
+
const c = await import('../integrations/composio/client.js');
|
|
4467
|
+
const result = await c.authorizeToolkit(slug, alias ? { alias } : undefined);
|
|
4468
|
+
res.json(result);
|
|
4469
|
+
}
|
|
4470
|
+
catch (err) {
|
|
4471
|
+
const c = await import('../integrations/composio/client.js');
|
|
4472
|
+
if (err instanceof c.ComposioNeedsAuthConfigError) {
|
|
4473
|
+
res.status(409).json({
|
|
4474
|
+
error: err.message,
|
|
4475
|
+
needsAuthConfig: true,
|
|
4476
|
+
toolkit: slug,
|
|
4477
|
+
setupUrl: 'https://platform.composio.dev/auth-configs',
|
|
4478
|
+
});
|
|
4479
|
+
return;
|
|
4480
|
+
}
|
|
4481
|
+
res.status(500).json({ error: String(err) });
|
|
4482
|
+
}
|
|
4483
|
+
});
|
|
4484
|
+
app.post('/api/composio/toolkits/:slug/disconnect', async (req, res) => {
|
|
4485
|
+
const connectionId = typeof req.body?.connectionId === 'string' ? req.body.connectionId : '';
|
|
4486
|
+
if (!connectionId) {
|
|
4487
|
+
res.status(400).json({ error: 'connectionId required in body' });
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4490
|
+
try {
|
|
4491
|
+
const c = await import('../integrations/composio/client.js');
|
|
4492
|
+
await c.disconnectToolkit(connectionId);
|
|
4493
|
+
res.json({ ok: true });
|
|
4494
|
+
}
|
|
4495
|
+
catch (err) {
|
|
4496
|
+
res.status(500).json({ error: String(err) });
|
|
4497
|
+
}
|
|
4498
|
+
});
|
|
4499
|
+
app.post('/api/composio/connections/:id/rename', async (req, res) => {
|
|
4500
|
+
const id = req.params.id;
|
|
4501
|
+
const alias = typeof req.body?.alias === 'string' ? req.body.alias.trim() : '';
|
|
4502
|
+
if (!alias) {
|
|
4503
|
+
res.status(400).json({ error: 'alias required in body' });
|
|
4504
|
+
return;
|
|
4505
|
+
}
|
|
4506
|
+
try {
|
|
4507
|
+
const c = await import('../integrations/composio/client.js');
|
|
4508
|
+
await c.renameConnection(id, alias);
|
|
4509
|
+
res.json({ ok: true });
|
|
4510
|
+
}
|
|
4511
|
+
catch (err) {
|
|
4512
|
+
res.status(500).json({ error: String(err) });
|
|
4513
|
+
}
|
|
4514
|
+
});
|
|
4389
4515
|
// ── CRON CRUD routes ──────────────────────────────────────────
|
|
4390
4516
|
app.get('/api/projects', (_req, res) => {
|
|
4391
4517
|
try {
|
|
@@ -4968,6 +5094,13 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4968
5094
|
{ key: 'MS_USER_EMAIL', label: 'User Email', hint: 'Email address for mail/calendar access' },
|
|
4969
5095
|
],
|
|
4970
5096
|
},
|
|
5097
|
+
{
|
|
5098
|
+
label: 'Composio',
|
|
5099
|
+
keys: [
|
|
5100
|
+
{ key: 'COMPOSIO_API_KEY', label: 'API Key', hint: 'Get one at https://app.composio.dev → Developers. Unlocks 1000+ services (Gmail, Slack, Notion, …) for the agent.', type: 'password' },
|
|
5101
|
+
{ key: 'COMPOSIO_USER_ID', label: 'User ID', hint: 'Optional — keys all connections under this Composio user. Defaults to "clementine-default".' },
|
|
5102
|
+
],
|
|
5103
|
+
},
|
|
4971
5104
|
{
|
|
4972
5105
|
label: 'Salesforce',
|
|
4973
5106
|
keys: [
|
|
@@ -5088,13 +5221,22 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5088
5221
|
const { setEnable1MContext } = await import('../config.js');
|
|
5089
5222
|
setEnable1MContext(value.toLowerCase() === 'true');
|
|
5090
5223
|
}
|
|
5224
|
+
// Composio: mutate process.env in-place + drop singleton so the very
|
|
5225
|
+
// next /api/composio/* call picks up the new key without a daemon
|
|
5226
|
+
// restart. Without this, "Save key → Connect Gmail" would 503 until
|
|
5227
|
+
// the user restarted, which defeats the dashboard-config UX.
|
|
5228
|
+
if (key === 'COMPOSIO_API_KEY' || key === 'COMPOSIO_USER_ID') {
|
|
5229
|
+
process.env[key] = value;
|
|
5230
|
+
const { resetComposioClient } = await import('../integrations/composio/client.js');
|
|
5231
|
+
resetComposioClient();
|
|
5232
|
+
}
|
|
5091
5233
|
res.json({ ok: true, message: `Updated ${key}` });
|
|
5092
5234
|
}
|
|
5093
5235
|
catch (err) {
|
|
5094
5236
|
res.status(500).json({ error: String(err) });
|
|
5095
5237
|
}
|
|
5096
5238
|
});
|
|
5097
|
-
app.delete('/api/settings/:key', (req, res) => {
|
|
5239
|
+
app.delete('/api/settings/:key', async (req, res) => {
|
|
5098
5240
|
try {
|
|
5099
5241
|
const { key } = req.params;
|
|
5100
5242
|
if (!existsSync(ENV_PATH)) {
|
|
@@ -5105,6 +5247,13 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5105
5247
|
const re = new RegExp(`^${key}=.*\n?`, 'm');
|
|
5106
5248
|
content = content.replace(re, '');
|
|
5107
5249
|
writeFileSync(ENV_PATH, content);
|
|
5250
|
+
// Hot-reload mirror of the PUT handler — drop process.env entry +
|
|
5251
|
+
// reset Composio client so removal takes effect without a restart.
|
|
5252
|
+
if (key === 'COMPOSIO_API_KEY' || key === 'COMPOSIO_USER_ID') {
|
|
5253
|
+
delete process.env[key];
|
|
5254
|
+
const { resetComposioClient } = await import('../integrations/composio/client.js');
|
|
5255
|
+
resetComposioClient();
|
|
5256
|
+
}
|
|
5108
5257
|
res.json({ ok: true, message: `Removed ${key}` });
|
|
5109
5258
|
}
|
|
5110
5259
|
catch (err) {
|
|
@@ -14325,6 +14474,15 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
14325
14474
|
</p>
|
|
14326
14475
|
</div>
|
|
14327
14476
|
<div class="tab-pane" id="tab-settings-integrations">
|
|
14477
|
+
<div class="card" style="margin-bottom:20px">
|
|
14478
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
14479
|
+
<span>Connections (via Composio)</span>
|
|
14480
|
+
<span style="font-size:11px;color:var(--text-muted)">1000+ services — OAuth handled by Composio</span>
|
|
14481
|
+
</div>
|
|
14482
|
+
<div class="card-body" style="padding:16px" id="composio-connections">
|
|
14483
|
+
<div class="empty-state">Loading...</div>
|
|
14484
|
+
</div>
|
|
14485
|
+
</div>
|
|
14328
14486
|
<div class="card" style="margin-bottom:20px">
|
|
14329
14487
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
14330
14488
|
<span>Claude Desktop Integrations</span>
|
|
@@ -15640,7 +15798,7 @@ function switchTab(group, tab) {
|
|
|
15640
15798
|
if (tab === 'learning' && typeof refreshSelfImprove === 'function') refreshSelfImprove();
|
|
15641
15799
|
}
|
|
15642
15800
|
if (group === 'settings') {
|
|
15643
|
-
if (tab === 'integrations') refreshSalesforce();
|
|
15801
|
+
if (tab === 'integrations') { refreshSalesforce(); refreshComposioConnections(); }
|
|
15644
15802
|
if (tab === 'remote') refreshRemoteAccess();
|
|
15645
15803
|
if (tab === 'security') refreshAuthSessions();
|
|
15646
15804
|
if (tab === 'projects' && typeof refreshProjects === 'function') refreshProjects();
|
|
@@ -25574,6 +25732,147 @@ async function refreshClaudeIntegrations(preloaded) {
|
|
|
25574
25732
|
}
|
|
25575
25733
|
}
|
|
25576
25734
|
|
|
25735
|
+
async function refreshComposioConnections() {
|
|
25736
|
+
var container = document.getElementById('composio-connections');
|
|
25737
|
+
if (!container) return;
|
|
25738
|
+
try {
|
|
25739
|
+
var r = await apiFetch('/api/composio/toolkits');
|
|
25740
|
+
var d = await r.json();
|
|
25741
|
+
if (!d.enabled) {
|
|
25742
|
+
container.innerHTML =
|
|
25743
|
+
'<div style="padding:8px 4px">' +
|
|
25744
|
+
'<div style="font-size:13px;margin-bottom:12px">Composio gives Clementine OAuth access to 1000+ services (Gmail, Slack, Notion, GitHub, …) from any channel — Telegram, Discord, dashboard chat. The agent never sees the credentials.</div>' +
|
|
25745
|
+
'<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px">Get your free API key at <a href="https://app.composio.dev/developers" target="_blank" style="color:var(--accent)">app.composio.dev/developers</a>, then paste it here:</div>' +
|
|
25746
|
+
'<div style="display:flex;gap:8px;align-items:center">' +
|
|
25747
|
+
'<input type="password" id="composio-key-input" placeholder="cak_..." style="flex:1;padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:13px;font-family:monospace">' +
|
|
25748
|
+
'<button class="btn btn-sm btn-primary" onclick="saveComposioApiKey()" style="font-size:12px">Save & Activate</button>' +
|
|
25749
|
+
'</div>' +
|
|
25750
|
+
'<div id="composio-key-status" style="font-size:11px;color:var(--text-muted);margin-top:8px"></div>' +
|
|
25751
|
+
'</div>';
|
|
25752
|
+
var input = document.getElementById('composio-key-input');
|
|
25753
|
+
if (input) input.addEventListener('keydown', function(e) { if (e.key === 'Enter') saveComposioApiKey(); });
|
|
25754
|
+
return;
|
|
25755
|
+
}
|
|
25756
|
+
var toolkits = d.toolkits || [];
|
|
25757
|
+
if (toolkits.length === 0) {
|
|
25758
|
+
container.innerHTML = '<div class="empty-state" style="padding:16px">No toolkits available. Check that your Composio API key is valid.</div>';
|
|
25759
|
+
return;
|
|
25760
|
+
}
|
|
25761
|
+
|
|
25762
|
+
var html = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px">';
|
|
25763
|
+
toolkits.forEach(function(t) {
|
|
25764
|
+
var connected = (t.connections || []).filter(function(c) { return c.status === 'ACTIVE'; });
|
|
25765
|
+
var pending = (t.connections || []).filter(function(c) { return c.status !== 'ACTIVE'; });
|
|
25766
|
+
var isConnected = connected.length > 0;
|
|
25767
|
+
var statusColor = isConnected ? 'var(--green)' : (pending.length > 0 ? 'var(--yellow,#f59e0b)' : 'var(--text-muted)');
|
|
25768
|
+
var byo = t.authMode === 'byo' && !t.hasAuthConfig;
|
|
25769
|
+
|
|
25770
|
+
html += '<div style="border:1px solid var(--border);border-radius:8px;padding:12px;background:var(--bg-secondary);display:flex;flex-direction:column;gap:8px">';
|
|
25771
|
+
html += '<div style="display:flex;align-items:center;gap:10px">';
|
|
25772
|
+
if (t.logoUrl) {
|
|
25773
|
+
html += '<img src="' + esc(t.logoUrl) + '" alt="" style="width:24px;height:24px;border-radius:4px;background:#fff;padding:2px;flex-shrink:0;object-fit:contain">';
|
|
25774
|
+
} else {
|
|
25775
|
+
html += '<div style="width:24px;height:24px;border-radius:4px;background:var(--bg-tertiary);flex-shrink:0"></div>';
|
|
25776
|
+
}
|
|
25777
|
+
html += '<div style="flex:1;min-width:0">';
|
|
25778
|
+
html += '<div style="font-size:13px;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(t.displayName) + '</div>';
|
|
25779
|
+
html += '<div style="font-size:10px;color:var(--text-muted)">' + (isConnected ? connected.length + ' account' + (connected.length !== 1 ? 's' : '') : 'Not connected') + '</div>';
|
|
25780
|
+
html += '</div>';
|
|
25781
|
+
html += '<span style="width:8px;height:8px;border-radius:50%;background:' + statusColor + ';flex-shrink:0"></span>';
|
|
25782
|
+
html += '</div>';
|
|
25783
|
+
|
|
25784
|
+
if (connected.length > 0) {
|
|
25785
|
+
html += '<div style="display:flex;flex-direction:column;gap:4px">';
|
|
25786
|
+
connected.forEach(function(c) {
|
|
25787
|
+
var label = c.alias || c.accountLabel || c.accountEmail || c.accountName || 'connected';
|
|
25788
|
+
html += '<div style="display:flex;align-items:center;gap:6px;font-size:11px">';
|
|
25789
|
+
html += '<span style="flex:1;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(label) + '</span>';
|
|
25790
|
+
html += '<button class="btn-sm" onclick="disconnectComposio(\\'' + esc(t.slug) + '\\',\\'' + esc(c.id) + '\\')" style="font-size:10px;padding:2px 6px;background:transparent;border:1px solid var(--border);color:var(--text-muted);border-radius:4px;cursor:pointer">Disconnect</button>';
|
|
25791
|
+
html += '</div>';
|
|
25792
|
+
});
|
|
25793
|
+
html += '</div>';
|
|
25794
|
+
}
|
|
25795
|
+
|
|
25796
|
+
if (byo) {
|
|
25797
|
+
html += '<div style="font-size:10px;color:var(--yellow,#f59e0b);background:rgba(245,158,11,0.08);padding:6px 8px;border-radius:4px;line-height:1.4">Needs a BYO OAuth app — <a href="https://platform.composio.dev/auth-configs" target="_blank" style="color:inherit;text-decoration:underline">set up in Composio</a> first.</div>';
|
|
25798
|
+
}
|
|
25799
|
+
|
|
25800
|
+
html += '<button class="btn btn-sm btn-primary" onclick="connectComposio(\\'' + esc(t.slug) + '\\')" style="font-size:11px">' + (isConnected ? '+ Add another account' : 'Connect') + '</button>';
|
|
25801
|
+
html += '</div>';
|
|
25802
|
+
});
|
|
25803
|
+
html += '</div>';
|
|
25804
|
+
container.innerHTML = html;
|
|
25805
|
+
} catch (e) {
|
|
25806
|
+
container.innerHTML = '<div class="empty-state" style="color:var(--red);padding:16px">Failed to load Composio toolkits: ' + esc(String(e)) + '</div>';
|
|
25807
|
+
}
|
|
25808
|
+
}
|
|
25809
|
+
|
|
25810
|
+
async function saveComposioApiKey() {
|
|
25811
|
+
var input = document.getElementById('composio-key-input');
|
|
25812
|
+
var status = document.getElementById('composio-key-status');
|
|
25813
|
+
if (!input) return;
|
|
25814
|
+
var key = (input.value || '').trim();
|
|
25815
|
+
if (!key) { if (status) { status.textContent = 'Enter a key first.'; status.style.color = 'var(--red)'; } return; }
|
|
25816
|
+
if (status) { status.textContent = 'Saving…'; status.style.color = 'var(--text-muted)'; }
|
|
25817
|
+
try {
|
|
25818
|
+
await apiJson('PUT', '/api/settings/COMPOSIO_API_KEY', { value: key });
|
|
25819
|
+
if (status) { status.textContent = 'Saved. Loading toolkits…'; status.style.color = 'var(--green)'; }
|
|
25820
|
+
toast('Composio API key saved', 'success');
|
|
25821
|
+
setTimeout(refreshComposioConnections, 400);
|
|
25822
|
+
} catch (e) {
|
|
25823
|
+
if (status) { status.textContent = 'Failed to save: ' + e; status.style.color = 'var(--red)'; }
|
|
25824
|
+
toast('Failed to save key: ' + e, 'error');
|
|
25825
|
+
}
|
|
25826
|
+
}
|
|
25827
|
+
|
|
25828
|
+
async function connectComposio(slug) {
|
|
25829
|
+
try {
|
|
25830
|
+
var res = await fetch('/api/composio/toolkits/' + encodeURIComponent(slug) + '/authorize', {
|
|
25831
|
+
method: 'POST',
|
|
25832
|
+
credentials: 'same-origin',
|
|
25833
|
+
headers: { 'content-type': 'application/json' },
|
|
25834
|
+
body: JSON.stringify({}),
|
|
25835
|
+
});
|
|
25836
|
+
var d = await res.json();
|
|
25837
|
+
if (res.status === 409 && d.needsAuthConfig) {
|
|
25838
|
+
toast('This toolkit needs a BYO OAuth app — opening Composio dashboard.', 'warn');
|
|
25839
|
+
window.open(d.setupUrl, '_blank');
|
|
25840
|
+
return;
|
|
25841
|
+
}
|
|
25842
|
+
if (!res.ok) { toast('Connect failed: ' + (d.error || res.status), 'error'); return; }
|
|
25843
|
+
if (d.redirectUrl) {
|
|
25844
|
+
window.open(d.redirectUrl, '_blank');
|
|
25845
|
+
toast('Opened ' + slug + ' authorization in a new tab. Refresh after approving.', 'info');
|
|
25846
|
+
// Soft poll to catch the new connection without forcing a manual refresh.
|
|
25847
|
+
setTimeout(refreshComposioConnections, 5000);
|
|
25848
|
+
setTimeout(refreshComposioConnections, 15000);
|
|
25849
|
+
setTimeout(refreshComposioConnections, 30000);
|
|
25850
|
+
} else {
|
|
25851
|
+
toast('Connected ' + slug, 'success');
|
|
25852
|
+
refreshComposioConnections();
|
|
25853
|
+
}
|
|
25854
|
+
} catch (e) { toast('Connect failed: ' + e, 'error'); }
|
|
25855
|
+
}
|
|
25856
|
+
|
|
25857
|
+
async function disconnectComposio(slug, connectionId) {
|
|
25858
|
+
if (!confirm('Disconnect this ' + slug + ' account?')) return;
|
|
25859
|
+
try {
|
|
25860
|
+
var res = await fetch('/api/composio/toolkits/' + encodeURIComponent(slug) + '/disconnect', {
|
|
25861
|
+
method: 'POST',
|
|
25862
|
+
credentials: 'same-origin',
|
|
25863
|
+
headers: { 'content-type': 'application/json' },
|
|
25864
|
+
body: JSON.stringify({ connectionId: connectionId }),
|
|
25865
|
+
});
|
|
25866
|
+
if (!res.ok) {
|
|
25867
|
+
var d = await res.json().catch(function() { return {}; });
|
|
25868
|
+
toast('Disconnect failed: ' + (d.error || res.status), 'error');
|
|
25869
|
+
return;
|
|
25870
|
+
}
|
|
25871
|
+
toast('Disconnected', 'success');
|
|
25872
|
+
refreshComposioConnections();
|
|
25873
|
+
} catch (e) { toast('Disconnect failed: ' + e, 'error'); }
|
|
25874
|
+
}
|
|
25875
|
+
|
|
25577
25876
|
async function refreshMcpServers() {
|
|
25578
25877
|
var container = document.getElementById('mcp-servers-list');
|
|
25579
25878
|
if (!container) return;
|
package/dist/cli/setup.js
CHANGED
|
@@ -176,6 +176,18 @@ const FEATURES = [
|
|
|
176
176
|
},
|
|
177
177
|
],
|
|
178
178
|
},
|
|
179
|
+
{
|
|
180
|
+
value: 'composio',
|
|
181
|
+
name: 'Composio (1000+ services — Gmail, Slack, Notion, GitHub, …)',
|
|
182
|
+
credentials: [
|
|
183
|
+
{
|
|
184
|
+
key: 'COMPOSIO_API_KEY',
|
|
185
|
+
label: 'Composio API key',
|
|
186
|
+
help: `Sign up at ${CYAN}https://app.composio.dev${DIM} (free tier) and copy your key from Developers > API Keys.\n After setup, manage connections in the dashboard: Settings > Integrations.`,
|
|
187
|
+
masked: true,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
},
|
|
179
191
|
{
|
|
180
192
|
value: 'outlook',
|
|
181
193
|
name: 'Outlook (Microsoft Graph — email + calendar)',
|
|
@@ -598,6 +610,7 @@ export async function runSetup() {
|
|
|
598
610
|
{ header: 'Voice', keys: ['GROQ_API_KEY', 'ELEVENLABS_API_KEY', 'ELEVENLABS_VOICE_ID'] },
|
|
599
611
|
{ header: 'Video', keys: ['GOOGLE_API_KEY'] },
|
|
600
612
|
{ header: 'Outlook (Microsoft Graph)', keys: ['MS_TENANT_ID', 'MS_CLIENT_ID', 'MS_CLIENT_SECRET', 'MS_USER_EMAIL'] },
|
|
613
|
+
{ header: 'Composio (1000+ services broker)', keys: ['COMPOSIO_API_KEY', 'COMPOSIO_USER_ID'] },
|
|
601
614
|
{ header: 'Workspace', keys: ['WORKSPACE_DIRS'] },
|
|
602
615
|
{ header: 'Security', keys: ['ALLOW_ALL_USERS'] },
|
|
603
616
|
];
|
|
@@ -189,6 +189,19 @@ export const INTEGRATIONS = [
|
|
|
189
189
|
{ envVar: 'GOOGLE_API_KEY', label: 'Gemini API key', required: true, docUrl: 'https://aistudio.google.com/apikey', persistent: true },
|
|
190
190
|
],
|
|
191
191
|
},
|
|
192
|
+
// ── Multi-service broker ──────────────────────────────────────────
|
|
193
|
+
{
|
|
194
|
+
slug: 'composio',
|
|
195
|
+
label: 'Composio',
|
|
196
|
+
description: 'OAuth + tool execution for 1000+ services (Gmail, Slack, Notion, GitHub, …). Connect toolkits in Settings → Integrations.',
|
|
197
|
+
kind: 'api-key',
|
|
198
|
+
docUrl: 'https://app.composio.dev/developers',
|
|
199
|
+
capabilities: ['Gmail', 'Google Calendar/Drive/Docs', 'Slack', 'Notion', 'GitHub', 'Linear', 'HubSpot', 'Salesforce', 'and 1000+ more'],
|
|
200
|
+
requirements: [
|
|
201
|
+
{ envVar: 'COMPOSIO_API_KEY', label: 'Composio API key', required: true, docUrl: 'https://app.composio.dev/developers', persistent: true },
|
|
202
|
+
{ envVar: 'COMPOSIO_USER_ID', label: 'User ID for Composio connections (defaults to clementine-default)', required: false, persistent: true },
|
|
203
|
+
],
|
|
204
|
+
},
|
|
192
205
|
// ── Payments ──────────────────────────────────────────────────────
|
|
193
206
|
{
|
|
194
207
|
slug: 'stripe',
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composio integration — client + connection management.
|
|
3
|
+
*
|
|
4
|
+
* Composio brokers OAuth and tool execution for 1000+ third-party services
|
|
5
|
+
* (Gmail, Slack, Notion, Linear, GitHub, …). Users authenticate once via
|
|
6
|
+
* Composio's hosted OAuth flow; Composio holds the tokens and refreshes them
|
|
7
|
+
* automatically. Clementine never sees the credentials.
|
|
8
|
+
*
|
|
9
|
+
* This module owns everything except the per-toolkit MCP server construction
|
|
10
|
+
* (see ./mcp-bridge.ts) and the dashboard endpoints (see cli/dashboard.ts).
|
|
11
|
+
*
|
|
12
|
+
* Disabled gracefully when COMPOSIO_API_KEY is not set — every public function
|
|
13
|
+
* returns an empty result, never throws.
|
|
14
|
+
*/
|
|
15
|
+
import { Composio } from '@composio/core';
|
|
16
|
+
import { ClaudeAgentSDKProvider } from '@composio/claude-agent-sdk';
|
|
17
|
+
export type ToolkitAuthMode = 'managed' | 'byo';
|
|
18
|
+
export interface CuratedToolkit {
|
|
19
|
+
slug: string;
|
|
20
|
+
displayName: string;
|
|
21
|
+
/** "managed" = Composio hosts the OAuth app — click Connect, it works.
|
|
22
|
+
* "byo" = User must register their own OAuth app on the toolkit's
|
|
23
|
+
* dev portal and add it as an Auth Config in Composio first. */
|
|
24
|
+
authMode: ToolkitAuthMode;
|
|
25
|
+
}
|
|
26
|
+
export declare const CURATED_TOOLKITS: CuratedToolkit[];
|
|
27
|
+
export declare function getComposio(): Composio<ClaudeAgentSDKProvider> | null;
|
|
28
|
+
export declare function isComposioEnabled(): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Discard the cached client + identity cache so the next call to getComposio()
|
|
31
|
+
* picks up a freshly-set COMPOSIO_API_KEY without a daemon restart. Called by
|
|
32
|
+
* the dashboard PUT /api/settings/COMPOSIO_API_KEY handler.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resetComposioClient(): void;
|
|
35
|
+
export declare function clementineUserId(): string;
|
|
36
|
+
export declare function displayNameFor(slug: string): string;
|
|
37
|
+
export interface ConnectedToolkit {
|
|
38
|
+
slug: string;
|
|
39
|
+
connectionId: string;
|
|
40
|
+
status: string;
|
|
41
|
+
alias?: string;
|
|
42
|
+
accountLabel?: string;
|
|
43
|
+
accountEmail?: string;
|
|
44
|
+
accountName?: string;
|
|
45
|
+
accountAvatarUrl?: string;
|
|
46
|
+
createdAt?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function listConnectedToolkits(): Promise<ConnectedToolkit[]>;
|
|
49
|
+
export interface ToolkitMeta {
|
|
50
|
+
slug: string;
|
|
51
|
+
name: string;
|
|
52
|
+
logo?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
toolsCount?: number;
|
|
55
|
+
}
|
|
56
|
+
export declare function listToolkitMeta(): Promise<Map<string, ToolkitMeta>>;
|
|
57
|
+
export declare function listToolkitSlugsWithAuthConfig(): Promise<Set<string>>;
|
|
58
|
+
export declare class ComposioNeedsAuthConfigError extends Error {
|
|
59
|
+
readonly slug: string;
|
|
60
|
+
readonly underlying: string;
|
|
61
|
+
constructor(slug: string, underlying: string);
|
|
62
|
+
}
|
|
63
|
+
export declare function authorizeToolkit(slug: string, opts?: {
|
|
64
|
+
callbackUrl?: string;
|
|
65
|
+
alias?: string;
|
|
66
|
+
}): Promise<{
|
|
67
|
+
redirectUrl: string | null;
|
|
68
|
+
connectionId: string;
|
|
69
|
+
}>;
|
|
70
|
+
export declare function disconnectToolkit(connectionId: string): Promise<void>;
|
|
71
|
+
export declare function renameConnection(connectionId: string, alias: string): Promise<void>;
|
|
72
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composio integration — client + connection management.
|
|
3
|
+
*
|
|
4
|
+
* Composio brokers OAuth and tool execution for 1000+ third-party services
|
|
5
|
+
* (Gmail, Slack, Notion, Linear, GitHub, …). Users authenticate once via
|
|
6
|
+
* Composio's hosted OAuth flow; Composio holds the tokens and refreshes them
|
|
7
|
+
* automatically. Clementine never sees the credentials.
|
|
8
|
+
*
|
|
9
|
+
* This module owns everything except the per-toolkit MCP server construction
|
|
10
|
+
* (see ./mcp-bridge.ts) and the dashboard endpoints (see cli/dashboard.ts).
|
|
11
|
+
*
|
|
12
|
+
* Disabled gracefully when COMPOSIO_API_KEY is not set — every public function
|
|
13
|
+
* returns an empty result, never throws.
|
|
14
|
+
*/
|
|
15
|
+
import { Composio } from '@composio/core';
|
|
16
|
+
import { ClaudeAgentSDKProvider } from '@composio/claude-agent-sdk';
|
|
17
|
+
import pino from 'pino';
|
|
18
|
+
const logger = pino({ name: 'clementine.composio' });
|
|
19
|
+
// Curated set surfaced in the dashboard. Composio exposes 1000+ — rendering
|
|
20
|
+
// them all is noisy. Users can still connect anything by editing this list.
|
|
21
|
+
export const CURATED_TOOLKITS = [
|
|
22
|
+
{ slug: 'gmail', displayName: 'Gmail', authMode: 'managed' },
|
|
23
|
+
{ slug: 'googlecalendar', displayName: 'Google Calendar', authMode: 'managed' },
|
|
24
|
+
{ slug: 'googledrive', displayName: 'Google Drive', authMode: 'managed' },
|
|
25
|
+
{ slug: 'googlesheets', displayName: 'Google Sheets', authMode: 'managed' },
|
|
26
|
+
{ slug: 'googledocs', displayName: 'Google Docs', authMode: 'managed' },
|
|
27
|
+
{ slug: 'slack', displayName: 'Slack', authMode: 'managed' },
|
|
28
|
+
{ slug: 'github', displayName: 'GitHub', authMode: 'managed' },
|
|
29
|
+
{ slug: 'linear', displayName: 'Linear', authMode: 'managed' },
|
|
30
|
+
{ slug: 'notion', displayName: 'Notion', authMode: 'managed' },
|
|
31
|
+
{ slug: 'hubspot', displayName: 'HubSpot', authMode: 'managed' },
|
|
32
|
+
{ slug: 'salesforce', displayName: 'Salesforce', authMode: 'managed' },
|
|
33
|
+
{ slug: 'discord', displayName: 'Discord', authMode: 'managed' },
|
|
34
|
+
{ slug: 'trello', displayName: 'Trello', authMode: 'managed' },
|
|
35
|
+
{ slug: 'asana', displayName: 'Asana', authMode: 'managed' },
|
|
36
|
+
{ slug: 'jira', displayName: 'Jira', authMode: 'managed' },
|
|
37
|
+
{ slug: 'airtable', displayName: 'Airtable', authMode: 'managed' },
|
|
38
|
+
{ slug: 'figma', displayName: 'Figma', authMode: 'managed' },
|
|
39
|
+
{ slug: 'dropbox', displayName: 'Dropbox', authMode: 'managed' },
|
|
40
|
+
{ slug: 'stripe', displayName: 'Stripe', authMode: 'managed' },
|
|
41
|
+
{ slug: 'supabase', displayName: 'Supabase', authMode: 'managed' },
|
|
42
|
+
{ slug: 'linkedin', displayName: 'LinkedIn', authMode: 'managed' },
|
|
43
|
+
{ slug: 'twitter', displayName: 'Twitter / X', authMode: 'byo' },
|
|
44
|
+
];
|
|
45
|
+
const DISPLAY_NAME_BY_SLUG = new Map(CURATED_TOOLKITS.map(t => [t.slug, t.displayName]));
|
|
46
|
+
let singleton = null;
|
|
47
|
+
export function getComposio() {
|
|
48
|
+
if (singleton)
|
|
49
|
+
return singleton;
|
|
50
|
+
const apiKey = process.env.COMPOSIO_API_KEY;
|
|
51
|
+
if (!apiKey)
|
|
52
|
+
return null;
|
|
53
|
+
singleton = new Composio({
|
|
54
|
+
apiKey,
|
|
55
|
+
provider: new ClaudeAgentSDKProvider(),
|
|
56
|
+
});
|
|
57
|
+
return singleton;
|
|
58
|
+
}
|
|
59
|
+
export function isComposioEnabled() {
|
|
60
|
+
return Boolean(process.env.COMPOSIO_API_KEY);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Discard the cached client + identity cache so the next call to getComposio()
|
|
64
|
+
* picks up a freshly-set COMPOSIO_API_KEY without a daemon restart. Called by
|
|
65
|
+
* the dashboard PUT /api/settings/COMPOSIO_API_KEY handler.
|
|
66
|
+
*/
|
|
67
|
+
export function resetComposioClient() {
|
|
68
|
+
singleton = null;
|
|
69
|
+
identityCache.clear();
|
|
70
|
+
toolkitMetaCache = null;
|
|
71
|
+
}
|
|
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.
|
|
74
|
+
export function clementineUserId() {
|
|
75
|
+
return process.env.COMPOSIO_USER_ID ?? 'clementine-default';
|
|
76
|
+
}
|
|
77
|
+
export function displayNameFor(slug) {
|
|
78
|
+
return DISPLAY_NAME_BY_SLUG.get(slug) ?? humanize(slug);
|
|
79
|
+
}
|
|
80
|
+
function humanize(slug) {
|
|
81
|
+
return slug
|
|
82
|
+
.split(/[-_]/g)
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.map(p => p.charAt(0).toUpperCase() + p.slice(1))
|
|
85
|
+
.join(' ');
|
|
86
|
+
}
|
|
87
|
+
function str(v) {
|
|
88
|
+
return typeof v === 'string' && v.trim() ? v.trim() : undefined;
|
|
89
|
+
}
|
|
90
|
+
function decodeJwtPayload(jwt) {
|
|
91
|
+
try {
|
|
92
|
+
const parts = jwt.split('.');
|
|
93
|
+
if (parts.length < 2)
|
|
94
|
+
return null;
|
|
95
|
+
const payload = parts[1];
|
|
96
|
+
const padded = payload + '==='.slice((payload.length + 3) % 4);
|
|
97
|
+
const b64 = padded.replace(/-/g, '+').replace(/_/g, '/');
|
|
98
|
+
return JSON.parse(Buffer.from(b64, 'base64').toString('utf-8'));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Composio's connected-account state is shaped per toolkit. Pull whatever
|
|
105
|
+
// human identity we can find — OAuth id_tokens (Google et al.) carry
|
|
106
|
+
// email/name/picture; other toolkits stash shop, subdomain, account_id.
|
|
107
|
+
function extractAccountIdentity(state, data) {
|
|
108
|
+
const s = (state && typeof state === 'object' ? state : {}) ?? {};
|
|
109
|
+
const d = (data && typeof data === 'object' ? data : {}) ?? {};
|
|
110
|
+
const out = {};
|
|
111
|
+
const idToken = str(s.id_token) ?? str(d.id_token);
|
|
112
|
+
if (idToken) {
|
|
113
|
+
const payload = decodeJwtPayload(idToken);
|
|
114
|
+
if (payload) {
|
|
115
|
+
out.email = str(payload.email);
|
|
116
|
+
out.name = str(payload.name) ?? str(payload.given_name);
|
|
117
|
+
out.avatarUrl = str(payload.picture);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const src of [d, s]) {
|
|
121
|
+
const profile = (src.user_info && typeof src.user_info === 'object' ? src.user_info : null) ??
|
|
122
|
+
(src.profile && typeof src.profile === 'object' ? src.profile : null);
|
|
123
|
+
if (profile) {
|
|
124
|
+
out.email = out.email ?? str(profile.email);
|
|
125
|
+
out.name = out.name ?? str(profile.name) ?? str(profile.display_name);
|
|
126
|
+
out.avatarUrl = out.avatarUrl ?? str(profile.picture) ?? str(profile.avatar_url);
|
|
127
|
+
}
|
|
128
|
+
out.email = out.email ?? str(src.email);
|
|
129
|
+
out.name = out.name ?? str(src.name) ?? str(src.display_name);
|
|
130
|
+
out.avatarUrl = out.avatarUrl ?? str(src.avatar_url) ?? str(src.picture);
|
|
131
|
+
}
|
|
132
|
+
const fallback = str(s.shop) ??
|
|
133
|
+
str(s.subdomain) ??
|
|
134
|
+
str(s.domain) ??
|
|
135
|
+
str(s.account_id) ??
|
|
136
|
+
str(s.site_name) ??
|
|
137
|
+
str(d.shop) ??
|
|
138
|
+
str(d.subdomain);
|
|
139
|
+
out.label = out.email ?? out.name ?? fallback;
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
function genericProfileParse(d) {
|
|
143
|
+
const first = (...candidates) => {
|
|
144
|
+
for (const c of candidates)
|
|
145
|
+
if (typeof c === 'string' && c.trim())
|
|
146
|
+
return c.trim();
|
|
147
|
+
return undefined;
|
|
148
|
+
};
|
|
149
|
+
const nested = (key) => (d[key] && typeof d[key] === 'object' ? d[key] : {});
|
|
150
|
+
const viewer = nested('viewer');
|
|
151
|
+
const user = nested('user');
|
|
152
|
+
const team = nested('team');
|
|
153
|
+
const profile = nested('profile');
|
|
154
|
+
const email = first(d.email, user.email, viewer.email, profile.email);
|
|
155
|
+
const name = first(d.name, d.login, d.display_name, user.name, viewer.name, profile.name, team.name);
|
|
156
|
+
const avatar = first(d.avatar_url, d.avatarUrl, d.picture, user.avatar_url, viewer.avatarUrl, profile.image);
|
|
157
|
+
return { email, name, avatarUrl: avatar, label: email ?? name };
|
|
158
|
+
}
|
|
159
|
+
const WHOAMI_BY_TOOLKIT = {
|
|
160
|
+
gmail: {
|
|
161
|
+
tool: 'GMAIL_GET_PROFILE',
|
|
162
|
+
arguments: { user_id: 'me' },
|
|
163
|
+
parse: d => {
|
|
164
|
+
const email = typeof d.emailAddress === 'string' ? d.emailAddress : undefined;
|
|
165
|
+
return { email, label: email };
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
github: { tool: 'GITHUB_GET_THE_AUTHENTICATED_USER', arguments: {}, parse: genericProfileParse },
|
|
169
|
+
linear: { tool: 'LINEAR_GET_CURRENT_USER', arguments: {}, parse: genericProfileParse },
|
|
170
|
+
notion: { tool: 'NOTION_GET_ABOUT_ME', arguments: {}, parse: genericProfileParse },
|
|
171
|
+
slack: { tool: 'SLACK_FETCH_TEAM_INFO', arguments: {}, parse: genericProfileParse },
|
|
172
|
+
hubspot: { tool: 'HUBSPOT_GET_ACCOUNT_INFO', arguments: {}, parse: genericProfileParse },
|
|
173
|
+
stripe: { tool: 'STRIPE_GET_ACCOUNT', arguments: {}, parse: genericProfileParse },
|
|
174
|
+
};
|
|
175
|
+
const identityCache = new Map();
|
|
176
|
+
const IDENTITY_TTL_MS = 15 * 60 * 1000;
|
|
177
|
+
async function fetchToolkitIdentity(composio, slug, connectedAccountId) {
|
|
178
|
+
const spec = WHOAMI_BY_TOOLKIT[slug];
|
|
179
|
+
if (!spec)
|
|
180
|
+
return {};
|
|
181
|
+
try {
|
|
182
|
+
const result = await composio.tools.execute(spec.tool, {
|
|
183
|
+
userId: clementineUserId(),
|
|
184
|
+
// Without this, Composio picks the user's *default* connection for the
|
|
185
|
+
// toolkit, so multiple Gmail accounts end up labeled with the same email.
|
|
186
|
+
...(connectedAccountId ? { connectedAccountId } : {}),
|
|
187
|
+
arguments: spec.arguments,
|
|
188
|
+
// Composio requires either a pinned toolkit version or this flag.
|
|
189
|
+
// Skipping pin so a toolkit bump doesn't break identity silently —
|
|
190
|
+
// misses fall through to fallback chain below.
|
|
191
|
+
dangerouslySkipVersionCheck: true,
|
|
192
|
+
});
|
|
193
|
+
if (!result.successful || !result.data)
|
|
194
|
+
return {};
|
|
195
|
+
return spec.parse(result.data);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
logger.warn({ err, slug }, 'whoami fetch failed');
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function getIdentityFor(composio, id, slug, seed) {
|
|
203
|
+
if (seed.label)
|
|
204
|
+
return seed;
|
|
205
|
+
const cached = identityCache.get(id);
|
|
206
|
+
if (cached && Date.now() - cached.at < IDENTITY_TTL_MS)
|
|
207
|
+
return cached.identity;
|
|
208
|
+
let identity = {};
|
|
209
|
+
try {
|
|
210
|
+
const full = await composio.connectedAccounts.get(id);
|
|
211
|
+
identity = extractAccountIdentity(full.state, full.data);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
logger.warn({ err, id }, 'failed to fetch full connection for identity');
|
|
215
|
+
}
|
|
216
|
+
if (!identity.label) {
|
|
217
|
+
const whoami = await fetchToolkitIdentity(composio, slug, id);
|
|
218
|
+
if (whoami.label)
|
|
219
|
+
identity = { ...identity, ...whoami };
|
|
220
|
+
}
|
|
221
|
+
identityCache.set(id, { at: Date.now(), identity });
|
|
222
|
+
return identity;
|
|
223
|
+
}
|
|
224
|
+
export async function listConnectedToolkits() {
|
|
225
|
+
const composio = getComposio();
|
|
226
|
+
if (!composio)
|
|
227
|
+
return [];
|
|
228
|
+
try {
|
|
229
|
+
const resp = await composio.connectedAccounts.list({ userIds: [clementineUserId()] });
|
|
230
|
+
const enriched = await Promise.all(resp.items.map(async (it) => {
|
|
231
|
+
const seed = extractAccountIdentity(it.state, it.data);
|
|
232
|
+
const identity = it.status === 'ACTIVE'
|
|
233
|
+
? await getIdentityFor(composio, it.id, it.toolkit.slug, seed)
|
|
234
|
+
: seed;
|
|
235
|
+
return {
|
|
236
|
+
slug: it.toolkit.slug,
|
|
237
|
+
connectionId: it.id,
|
|
238
|
+
status: it.status,
|
|
239
|
+
alias: it.alias ?? undefined,
|
|
240
|
+
accountLabel: identity.label,
|
|
241
|
+
accountEmail: identity.email,
|
|
242
|
+
accountName: identity.name,
|
|
243
|
+
accountAvatarUrl: identity.avatarUrl,
|
|
244
|
+
createdAt: it.createdAt,
|
|
245
|
+
};
|
|
246
|
+
}));
|
|
247
|
+
return enriched;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
logger.error({ err }, 'listConnectedToolkits failed');
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
let toolkitMetaCache = null;
|
|
255
|
+
async function fetchAllToolkitMeta() {
|
|
256
|
+
const composio = getComposio();
|
|
257
|
+
if (!composio)
|
|
258
|
+
return new Map();
|
|
259
|
+
const out = new Map();
|
|
260
|
+
try {
|
|
261
|
+
const resp = await composio.toolkits.get({ limit: 500 });
|
|
262
|
+
const items = Array.isArray(resp) ? resp : (resp.items ?? []);
|
|
263
|
+
for (const it of items) {
|
|
264
|
+
out.set(it.slug, {
|
|
265
|
+
slug: it.slug,
|
|
266
|
+
name: it.name,
|
|
267
|
+
logo: it.meta?.logo,
|
|
268
|
+
description: it.meta?.description,
|
|
269
|
+
toolsCount: it.meta?.toolsCount,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
// Backfill curated toolkits the list endpoint omitted (e.g. MCP-only ones).
|
|
273
|
+
await Promise.all(CURATED_TOOLKITS.filter(t => !out.has(t.slug)).map(async (t) => {
|
|
274
|
+
try {
|
|
275
|
+
const full = (await composio.toolkits.get(t.slug));
|
|
276
|
+
out.set(full.slug, {
|
|
277
|
+
slug: full.slug,
|
|
278
|
+
name: full.name,
|
|
279
|
+
logo: full.meta?.logo,
|
|
280
|
+
description: full.meta?.description,
|
|
281
|
+
toolsCount: full.meta?.toolsCount,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
logger.debug({ err, slug: t.slug }, 'meta backfill failed');
|
|
286
|
+
}
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
logger.error({ err }, 'fetchAllToolkitMeta failed');
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
export async function listToolkitMeta() {
|
|
295
|
+
if (!toolkitMetaCache) {
|
|
296
|
+
toolkitMetaCache = fetchAllToolkitMeta().catch(err => {
|
|
297
|
+
logger.error({ err }, 'listToolkitMeta failed');
|
|
298
|
+
toolkitMetaCache = null;
|
|
299
|
+
return new Map();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return toolkitMetaCache;
|
|
303
|
+
}
|
|
304
|
+
export async function listToolkitSlugsWithAuthConfig() {
|
|
305
|
+
const composio = getComposio();
|
|
306
|
+
if (!composio)
|
|
307
|
+
return new Set();
|
|
308
|
+
try {
|
|
309
|
+
const resp = await composio.authConfigs.list({ limit: 200 });
|
|
310
|
+
return new Set(resp.items.map(it => it.toolkit.slug));
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
logger.error({ err }, 'listToolkitSlugsWithAuthConfig failed');
|
|
314
|
+
return new Set();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// ── Authorize / disconnect ─────────────────────────────────────────────
|
|
318
|
+
export class ComposioNeedsAuthConfigError extends Error {
|
|
319
|
+
slug;
|
|
320
|
+
underlying;
|
|
321
|
+
constructor(slug, underlying) {
|
|
322
|
+
super(`Toolkit "${slug}" needs an auth config — Composio doesn't host a managed OAuth app for it. ` +
|
|
323
|
+
`Add it via the Composio Dashboard: Toolkits → search ${slug} → Add to project → paste your OAuth credentials. ` +
|
|
324
|
+
`https://platform.composio.dev/auth-configs`);
|
|
325
|
+
this.slug = slug;
|
|
326
|
+
this.underlying = underlying;
|
|
327
|
+
this.name = 'ComposioNeedsAuthConfigError';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export async function authorizeToolkit(slug, opts) {
|
|
331
|
+
const composio = getComposio();
|
|
332
|
+
if (!composio)
|
|
333
|
+
throw new Error('COMPOSIO_API_KEY not set');
|
|
334
|
+
// 1. Find or create an auth config. session.authorize() doesn't auto-create
|
|
335
|
+
// so we have to pass authConfigId explicitly to connectedAccounts.initiate.
|
|
336
|
+
let authConfigId;
|
|
337
|
+
const existingConfig = (await composio.authConfigs.list({ toolkit: slug })).items[0];
|
|
338
|
+
if (existingConfig) {
|
|
339
|
+
authConfigId = existingConfig.id;
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
try {
|
|
343
|
+
const created = await composio.authConfigs.create(slug, {
|
|
344
|
+
type: 'use_composio_managed_auth',
|
|
345
|
+
name: `${displayNameFor(slug)} Auth Config`,
|
|
346
|
+
});
|
|
347
|
+
authConfigId = created.id;
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
// 400 → Composio doesn't host a managed OAuth app for this toolkit;
|
|
351
|
+
// user must register their own via the Composio dashboard.
|
|
352
|
+
const status = err?.status;
|
|
353
|
+
if (status === 400) {
|
|
354
|
+
throw new ComposioNeedsAuthConfigError(slug, String(err));
|
|
355
|
+
}
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// 2. Initiate the connection. allowMultiple if there's already an active
|
|
360
|
+
// connection so we add another account instead of replacing.
|
|
361
|
+
const existing = (await listConnectedToolkits()).filter(c => c.slug === slug && c.status === 'ACTIVE');
|
|
362
|
+
const conn = await composio.connectedAccounts.initiate(clementineUserId(), authConfigId, {
|
|
363
|
+
...(existing.length > 0 ? { allowMultiple: true } : {}),
|
|
364
|
+
...(opts?.callbackUrl ? { callbackUrl: opts.callbackUrl } : {}),
|
|
365
|
+
...(opts?.alias ? { alias: opts.alias } : {}),
|
|
366
|
+
});
|
|
367
|
+
return { redirectUrl: conn.redirectUrl ?? null, connectionId: conn.id };
|
|
368
|
+
}
|
|
369
|
+
export async function disconnectToolkit(connectionId) {
|
|
370
|
+
const composio = getComposio();
|
|
371
|
+
if (!composio)
|
|
372
|
+
throw new Error('COMPOSIO_API_KEY not set');
|
|
373
|
+
await composio.connectedAccounts.delete(connectionId);
|
|
374
|
+
identityCache.delete(connectionId);
|
|
375
|
+
}
|
|
376
|
+
export async function renameConnection(connectionId, alias) {
|
|
377
|
+
const composio = getComposio();
|
|
378
|
+
if (!composio)
|
|
379
|
+
throw new Error('COMPOSIO_API_KEY not set');
|
|
380
|
+
// The Composio high-level wrapper only exposes status updates; alias
|
|
381
|
+
// rename lives on the raw client's PATCH endpoint, which is marked
|
|
382
|
+
// protected. Bridge through `as any` — this is a small, well-scoped escape
|
|
383
|
+
// hatch and the alternative (bypassing the wrapper entirely) loses retry
|
|
384
|
+
// and auth handling.
|
|
385
|
+
await composio.client.connectedAccounts.patch(connectionId, { alias });
|
|
386
|
+
}
|
|
387
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composio MCP bridge — converts Composio toolkits into in-process SDK MCP
|
|
3
|
+
* servers that the Claude Agent SDK's `query()` can spawn directly.
|
|
4
|
+
*
|
|
5
|
+
* Why per-toolkit servers (instead of one big "composio" server)?
|
|
6
|
+
* - Per-agent scoping. A scheduler-bot doesn't need GitHub tools; spawning
|
|
7
|
+
* a Calendar-only sub-agent shouldn't drag in 200 tools across 20
|
|
8
|
+
* toolkits.
|
|
9
|
+
* - Tool-name namespacing. Each MCP server registers its tools under its
|
|
10
|
+
* own name, so we get `mcp__gmail__GMAIL_SEND_EMAIL` not
|
|
11
|
+
* `mcp__composio__GMAIL_SEND_EMAIL` collisions across toolkits.
|
|
12
|
+
* - Concurrency. Composio's TS provider opens one session per server, so
|
|
13
|
+
* having one server per toolkit lets unrelated toolkits load in parallel.
|
|
14
|
+
*
|
|
15
|
+
* Returns an empty map when COMPOSIO_API_KEY is unset, so the agent path
|
|
16
|
+
* always works — Composio is purely additive.
|
|
17
|
+
*/
|
|
18
|
+
import type { McpSdkServerConfigWithInstance } from '@anthropic-ai/claude-agent-sdk';
|
|
19
|
+
/**
|
|
20
|
+
* Build SDK MCP server configs for the given toolkit slugs (or all active
|
|
21
|
+
* connected toolkits when omitted). Each toolkit becomes one MCP server.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildComposioMcpServers(slugs?: string[]): Promise<Record<string, McpSdkServerConfigWithInstance>>;
|
|
24
|
+
//# sourceMappingURL=mcp-bridge.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composio MCP bridge — converts Composio toolkits into in-process SDK MCP
|
|
3
|
+
* servers that the Claude Agent SDK's `query()` can spawn directly.
|
|
4
|
+
*
|
|
5
|
+
* Why per-toolkit servers (instead of one big "composio" server)?
|
|
6
|
+
* - Per-agent scoping. A scheduler-bot doesn't need GitHub tools; spawning
|
|
7
|
+
* a Calendar-only sub-agent shouldn't drag in 200 tools across 20
|
|
8
|
+
* toolkits.
|
|
9
|
+
* - Tool-name namespacing. Each MCP server registers its tools under its
|
|
10
|
+
* own name, so we get `mcp__gmail__GMAIL_SEND_EMAIL` not
|
|
11
|
+
* `mcp__composio__GMAIL_SEND_EMAIL` collisions across toolkits.
|
|
12
|
+
* - Concurrency. Composio's TS provider opens one session per server, so
|
|
13
|
+
* having one server per toolkit lets unrelated toolkits load in parallel.
|
|
14
|
+
*
|
|
15
|
+
* Returns an empty map when COMPOSIO_API_KEY is unset, so the agent path
|
|
16
|
+
* always works — Composio is purely additive.
|
|
17
|
+
*/
|
|
18
|
+
import { createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
|
|
19
|
+
import pino from 'pino';
|
|
20
|
+
import { clementineUserId, getComposio, listConnectedToolkits, } from './client.js';
|
|
21
|
+
const logger = pino({ name: 'clementine.composio.mcp' });
|
|
22
|
+
/**
|
|
23
|
+
* Build SDK MCP server configs for the given toolkit slugs (or all active
|
|
24
|
+
* connected toolkits when omitted). Each toolkit becomes one MCP server.
|
|
25
|
+
*/
|
|
26
|
+
export async function buildComposioMcpServers(slugs) {
|
|
27
|
+
const composio = getComposio();
|
|
28
|
+
if (!composio)
|
|
29
|
+
return {};
|
|
30
|
+
const connected = await listConnectedToolkits();
|
|
31
|
+
const activeSlugs = new Set(connected.filter(c => c.status === 'ACTIVE').map(c => c.slug));
|
|
32
|
+
const targetSlugs = slugs?.length
|
|
33
|
+
? slugs.filter(s => activeSlugs.has(s))
|
|
34
|
+
: [...activeSlugs];
|
|
35
|
+
if (targetSlugs.length === 0)
|
|
36
|
+
return {};
|
|
37
|
+
const out = {};
|
|
38
|
+
// Build serially to avoid hammering Composio with parallel session opens
|
|
39
|
+
// on every agent query. Sessions are cached upstream by Composio's SDK,
|
|
40
|
+
// so repeat calls within a process are cheap after the first hit.
|
|
41
|
+
for (const slug of targetSlugs) {
|
|
42
|
+
try {
|
|
43
|
+
out[slug] = await buildOne(composio, slug, connected);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
logger.warn({ err, slug }, 'failed to build Composio MCP server — skipping');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
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];
|
|
60
|
+
const session = await composio.create(clementineUserId(), {
|
|
61
|
+
toolkits: [slug],
|
|
62
|
+
manageConnections: false,
|
|
63
|
+
...(authConfig ? { authConfigs: { [slug]: authConfig.id } } : {}),
|
|
64
|
+
...(activeCount >= 2
|
|
65
|
+
? { multiAccount: { enable: true, requireExplicitSelection: true } }
|
|
66
|
+
: {}),
|
|
67
|
+
});
|
|
68
|
+
const tools = await session.tools();
|
|
69
|
+
return createSdkMcpServer({
|
|
70
|
+
name: slug,
|
|
71
|
+
version: '0.1.0',
|
|
72
|
+
tools,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=mcp-bridge.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clementine-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Clementine — Personal AI Assistant (TypeScript)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@anthropic-ai/claude-agent-sdk": "^0.2.119",
|
|
25
25
|
"@anthropic-ai/sdk": "^0.91.0",
|
|
26
|
+
"@composio/claude-agent-sdk": "^0.8.1",
|
|
27
|
+
"@composio/core": "^0.8.1",
|
|
26
28
|
"@inquirer/prompts": "^7.0.0",
|
|
27
29
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
30
|
"@slack/bolt": "^4.2.0",
|