clementine-agent 1.0.44 → 1.0.46
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 +68 -67
- package/dist/agent/mcp-bridge.d.ts +16 -0
- package/dist/agent/mcp-bridge.js +67 -0
- package/dist/config/integrations-registry.d.ts +80 -0
- package/dist/config/integrations-registry.js +268 -0
- package/dist/index.js +11 -1
- package/dist/secrets/auth-profiles.d.ts +57 -0
- package/dist/secrets/auth-profiles.js +111 -0
- package/dist/secrets/keychain.d.ts +35 -0
- package/dist/secrets/keychain.js +119 -0
- package/dist/secrets/resolver.d.ts +34 -0
- package/dist/secrets/resolver.js +70 -0
- package/dist/tools/admin-tools.js +131 -8
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -1052,34 +1052,39 @@ Obsidian vault with YAML frontmatter, [[wikilinks]], #tags.
|
|
|
1052
1052
|
**Remembering:** Durable facts → memory_write(action="update_memory"). Daily context → note_take / memory_write(action="append_daily"). New person → note_create. New task → task_add.
|
|
1053
1053
|
Save important facts immediately; a background agent also extracts after each exchange.
|
|
1054
1054
|
|
|
1055
|
-
## Self-Configuration (
|
|
1055
|
+
## Self-Configuration (never tell ${owner} to edit a config file)
|
|
1056
1056
|
|
|
1057
|
-
|
|
1057
|
+
Clementine is self-configuring. Every credential, every integration, every tool permission can be set by calling a tool — no hand-editing.
|
|
1058
1058
|
|
|
1059
|
-
|
|
1059
|
+
### Integrations (Slack, Notion, Stripe, Salesforce, etc.)
|
|
1060
1060
|
|
|
1061
|
-
|
|
1061
|
+
You have a declarative registry of every integration you can configure. Use it:
|
|
1062
1062
|
|
|
1063
|
-
|
|
1063
|
+
- \`list_integrations\` — shows every integration you know about
|
|
1064
|
+
- \`integration_status [slug]\` — reports which are configured, partial (some creds missing), or missing entirely
|
|
1065
|
+
- \`setup_integration <slug>\` — returns the required env vars, doc URLs, and current status — use this BEFORE asking ${owner} for any credential
|
|
1066
|
+
- \`auth_profile_status\` — shows stored OAuth profiles and token expiry
|
|
1064
1067
|
|
|
1065
|
-
**
|
|
1066
|
-
- They DO work from agent/daemon contexts. M365 has been used successfully for weeks from here, through the exact same code path.
|
|
1067
|
-
- If your first call is refused, the reason is **only** that the specific tool name isn't in your current allowedTools. Nothing to do with Claude Desktop vs. agent contexts. Nothing to do with the integrations.json file having an empty tools array.
|
|
1068
|
-
- The fix is **always** the same: \`allow_tool("mcp__claude_ai_<IntegrationName>__<tool_name>")\`, then retry. Example tool names: \`mcp__claude_ai_Google_Drive__search_files\`, \`mcp__claude_ai_Google_Drive__list_recent_files\`, \`mcp__claude_ai_Google_Drive__read_file_content\`.
|
|
1068
|
+
**When ${owner} says "set up X":** always call \`setup_integration(x)\` first. It returns the exact env var names and where to get each one. Then walk ${owner} through each missing credential one at a time, saving each with \`env_set\` as they provide it. After the last one, call \`integration_status\` to confirm "configured".
|
|
1069
1069
|
|
|
1070
|
-
**
|
|
1071
|
-
- "Google Drive isn't available to agents"
|
|
1072
|
-
- "connector tools only work in Claude Desktop chat"
|
|
1073
|
-
- "please open Claude Desktop and try there first"
|
|
1074
|
-
- "the integrations.json has an empty tools array, so I can't use it"
|
|
1075
|
-
- "you need to edit my config file"
|
|
1076
|
-
- "the daemon needs someone to add this"
|
|
1070
|
+
**Never invent env var names.** If an integration isn't in the registry, say so and ask ${owner} to confirm which one they mean — don't guess \`STRIPE_SECRET\` when the registry says \`STRIPE_SECRET_KEY\`.
|
|
1077
1071
|
|
|
1078
|
-
|
|
1072
|
+
### Saving credentials
|
|
1079
1073
|
|
|
1080
|
-
|
|
1074
|
+
\`env_set(key, value)\` — the one tool for saving any API key, token, or config. On macOS it defaults to the login Keychain (secure); elsewhere it falls back to plaintext \`~/.clementine/.env\`. \`process.env\` is updated immediately — the next tool call can use the value. No restart needed unless a long-lived channel adapter needs re-auth.
|
|
1081
1075
|
|
|
1082
|
-
|
|
1076
|
+
Companion tools: \`env_list\` (masked values + backend), \`env_unset\` (removes + clears Keychain entry).
|
|
1077
|
+
|
|
1078
|
+
### When a tool call is refused
|
|
1079
|
+
|
|
1080
|
+
If any tool call fails with "not in my function schema" / "tool not allowed" / "unknown tool" while the tool appears in your SDK inventory:
|
|
1081
|
+
|
|
1082
|
+
1. Call \`allow_tool("<exact_tool_name>")\` — persists to your whitelist
|
|
1083
|
+
2. Retry the original call
|
|
1084
|
+
|
|
1085
|
+
Takes effect on your next query. **Never** respond with "this tool isn't available to agents," "you need to edit my config," or "try it in a different app." If the tool name exists, you can add it.
|
|
1086
|
+
|
|
1087
|
+
\`list_allowed_tools\` / \`disallow_tool\` manage the list.
|
|
1083
1088
|
|
|
1084
1089
|
## Context Window Management
|
|
1085
1090
|
|
|
@@ -1094,40 +1099,21 @@ Delegate data-heavy work (SEO, analytics, bulk API calls for 3+ entities) to sub
|
|
|
1094
1099
|
Never spawn a sub-agent with vague instructions like "handle this brief" — tell it exactly what to read, what to change, and where to write the result.
|
|
1095
1100
|
`);
|
|
1096
1101
|
}
|
|
1097
|
-
// Inject Claude Desktop integration awareness.
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
// - Possibly-available: common integrations the owner may have connected
|
|
1101
|
-
// at claude.ai level that we don't have a usage record for yet. The
|
|
1102
|
-
// integrations-file is written reactively — only entries with
|
|
1103
|
-
// successful tool uses get captured — so a freshly-connected
|
|
1104
|
-
// Google Drive / Gmail / Slack connector is invisible to us until we
|
|
1105
|
-
// try it. Hint the agent that it can try these blindly; the tool
|
|
1106
|
-
// call will either succeed or return an auth error, at which point
|
|
1107
|
-
// the record gets captured.
|
|
1102
|
+
// Inject Claude Desktop integration awareness. Derived from the probed
|
|
1103
|
+
// SDK tool inventory — whatever the current user has connected at the
|
|
1104
|
+
// claude.ai level shows up here automatically, no per-install hardcoding.
|
|
1108
1105
|
try {
|
|
1109
|
-
const
|
|
1110
|
-
const
|
|
1111
|
-
if (
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
['
|
|
1120
|
-
['Google_Calendar', 'Google Calendar'], ['Google_Workspace', 'Google Workspace'],
|
|
1121
|
-
['Slack', 'Slack'], ['Notion', 'Notion'], ['GitHub', 'GitHub'],
|
|
1122
|
-
['Linear', 'Linear'], ['Asana', 'Asana'], ['Jira', 'Jira'],
|
|
1123
|
-
['Dropbox', 'Dropbox'], ['Salesforce', 'Salesforce'],
|
|
1124
|
-
['Microsoft_365', 'Microsoft 365'],
|
|
1125
|
-
];
|
|
1126
|
-
const maybe = SPECULATIVE.filter(([name]) => !knownNames.has(name)).map(([, label]) => label);
|
|
1127
|
-
if (maybe.length > 0) {
|
|
1128
|
-
parts.push(`**Possibly connected (try them — they'll either work or return an auth error):** ${maybe.join(', ')}. ` +
|
|
1129
|
-
`Tool names follow \`mcp__claude_ai_<IntegrationName>__<tool>\` — e.g. \`mcp__claude_ai_Google_Drive__search_files\`, \`mcp__claude_ai_Gmail__authenticate\`. ` +
|
|
1130
|
-
`Don't tell ${owner} an integration is "not available" without first attempting the tool call — a fresh connection won't be in your recorded list until after first use.`);
|
|
1106
|
+
const inv = _mcpBridge?.loadToolInventory();
|
|
1107
|
+
const labels = new Set();
|
|
1108
|
+
if (inv?.tools) {
|
|
1109
|
+
for (const t of inv.tools) {
|
|
1110
|
+
const m = t.match(/^mcp__claude_ai_([^_]+(?:_[^_]+)*)__/);
|
|
1111
|
+
if (m)
|
|
1112
|
+
labels.add(m[1].replace(/_/g, ' '));
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (labels.size > 0) {
|
|
1116
|
+
parts.push(`**Claude Desktop integrations connected for this user:** ${[...labels].sort().join(', ')}. Call \`mcp__claude_ai_<Integration>__<tool>\` directly; the tool names and schemas are in your SDK inventory.`);
|
|
1131
1117
|
}
|
|
1132
1118
|
}
|
|
1133
1119
|
catch { /* non-fatal */ }
|
|
@@ -1331,6 +1317,18 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1331
1317
|
}
|
|
1332
1318
|
// Security rules are now appended to systemPrompt in buildOptions()
|
|
1333
1319
|
// Volatile suffix — put last so the stable prefix above stays cache-friendly.
|
|
1320
|
+
// Integration status — injected here (not in the stable prefix) because
|
|
1321
|
+
// it changes as ${owner} configures new credentials, and we don't want
|
|
1322
|
+
// every env_set to invalidate the cache.
|
|
1323
|
+
if (!isAutonomous) {
|
|
1324
|
+
try {
|
|
1325
|
+
const { summarizeIntegrationStatus } = require('../config/integrations-registry.js');
|
|
1326
|
+
const summary = summarizeIntegrationStatus(process.env);
|
|
1327
|
+
if (summary)
|
|
1328
|
+
parts.push(`## Integration Status\n\n${summary}\n\nCall \`integration_status\`, \`list_integrations\`, or \`setup_integration\` for details.`);
|
|
1329
|
+
}
|
|
1330
|
+
catch { /* non-fatal */ }
|
|
1331
|
+
}
|
|
1334
1332
|
const channel = deriveChannel({ sessionKey, isAutonomous, cronTier });
|
|
1335
1333
|
const resolvedModel = resolveModel(model) ?? MODEL;
|
|
1336
1334
|
const modelLabel = Object.entries(MODELS).find(([, v]) => v === resolvedModel)?.[0] ?? resolvedModel;
|
|
@@ -1387,6 +1385,11 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1387
1385
|
mcpTool('env_set'),
|
|
1388
1386
|
mcpTool('env_list'),
|
|
1389
1387
|
mcpTool('env_unset'),
|
|
1388
|
+
// Integration registry — proactive configuration
|
|
1389
|
+
mcpTool('integration_status'),
|
|
1390
|
+
mcpTool('list_integrations'),
|
|
1391
|
+
mcpTool('setup_integration'),
|
|
1392
|
+
mcpTool('auth_profile_status'),
|
|
1390
1393
|
// Self-service tool whitelist — Clementine can add tools she discovers
|
|
1391
1394
|
// in the SDK init inventory but that aren't in her baseline allowedTools
|
|
1392
1395
|
mcpTool('allow_tool'),
|
|
@@ -1449,28 +1452,26 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1449
1452
|
}
|
|
1450
1453
|
}
|
|
1451
1454
|
catch { /* non-fatal — dynamic tools are supplementary */ }
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1455
|
-
//
|
|
1456
|
-
//
|
|
1457
|
-
//
|
|
1458
|
-
// the whitelist matches reality.
|
|
1455
|
+
// Merge the SDK's probed tool inventory. On daemon startup we run a
|
|
1456
|
+
// one-shot query with no whitelist to discover every tool Claude Code
|
|
1457
|
+
// surfaces (mcp__claude_ai_* connectors, plugins, custom MCP servers,
|
|
1458
|
+
// built-ins). The inventory is cached 24h. This makes the whitelist
|
|
1459
|
+
// match whatever the *current user* has connected — no per-install
|
|
1460
|
+
// hardcoding of tool names in the system prompt or code.
|
|
1459
1461
|
try {
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
for (const
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
allowedTools.push(fullName);
|
|
1462
|
+
const inv = _mcpBridge?.loadToolInventory();
|
|
1463
|
+
if (inv && Array.isArray(inv.tools)) {
|
|
1464
|
+
for (const t of inv.tools) {
|
|
1465
|
+
if (typeof t === 'string' && !allowedTools.includes(t))
|
|
1466
|
+
allowedTools.push(t);
|
|
1466
1467
|
}
|
|
1467
1468
|
}
|
|
1468
1469
|
}
|
|
1469
1470
|
catch { /* non-fatal */ }
|
|
1470
1471
|
// Self-service extension — Clementine can add tools to her own whitelist
|
|
1471
1472
|
// at runtime via the `allow_tool` MCP tool, writing to allowed-tools-
|
|
1472
|
-
// extra.json.
|
|
1473
|
-
//
|
|
1473
|
+
// extra.json. Covers the case where a user connects a new integration
|
|
1474
|
+
// between daemon startup probes (< 24h window).
|
|
1474
1475
|
try {
|
|
1475
1476
|
const extraPath = path.join(BASE_DIR, 'allowed-tools-extra.json');
|
|
1476
1477
|
if (fs.existsSync(extraPath)) {
|
|
@@ -54,6 +54,22 @@ export declare function loadClaudeIntegrations(): Record<string, ClaudeIntegrati
|
|
|
54
54
|
export declare function recordClaudeIntegrationUse(toolName: string): void;
|
|
55
55
|
/** Get all discovered Claude Desktop integrations as a list. */
|
|
56
56
|
export declare function getClaudeIntegrations(): ClaudeIntegration[];
|
|
57
|
+
export interface ToolInventory {
|
|
58
|
+
/** ISO timestamp of the probe */
|
|
59
|
+
probedAt: string;
|
|
60
|
+
/** Full list of tool names the SDK reported in init.tools when no whitelist was applied */
|
|
61
|
+
tools: string[];
|
|
62
|
+
}
|
|
63
|
+
export declare function loadToolInventory(): ToolInventory | null;
|
|
64
|
+
/**
|
|
65
|
+
* Run a minimal SDK query with NO tool whitelist so the init message
|
|
66
|
+
* returns every tool Claude Code is actually surfacing — claude_ai_*
|
|
67
|
+
* connectors, plugins, built-ins, custom MCP servers. Cached for 24h.
|
|
68
|
+
* This removes any need to hardcode user-specific tool names in the
|
|
69
|
+
* system prompt; whatever Claude Desktop is currently connecting to the
|
|
70
|
+
* user's account becomes automatically available to the agent.
|
|
71
|
+
*/
|
|
72
|
+
export declare function probeAvailableTools(force?: boolean): Promise<ToolInventory>;
|
|
57
73
|
/**
|
|
58
74
|
* Register every integration found in a tool inventory. The SDK's system
|
|
59
75
|
* init message (subtype='init') includes a `tools: string[]` with the full
|
package/dist/agent/mcp-bridge.js
CHANGED
|
@@ -384,6 +384,73 @@ export function recordClaudeIntegrationUse(toolName) {
|
|
|
384
384
|
export function getClaudeIntegrations() {
|
|
385
385
|
return Object.values(loadClaudeIntegrations());
|
|
386
386
|
}
|
|
387
|
+
// ── SDK tool inventory probe ───────────────────────────────────────────
|
|
388
|
+
const TOOL_INVENTORY_FILE = path.join(BASE_DIR, '.tool-inventory.json');
|
|
389
|
+
const TOOL_INVENTORY_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24h
|
|
390
|
+
export function loadToolInventory() {
|
|
391
|
+
try {
|
|
392
|
+
if (!existsSync(TOOL_INVENTORY_FILE))
|
|
393
|
+
return null;
|
|
394
|
+
const data = JSON.parse(readFileSync(TOOL_INVENTORY_FILE, 'utf-8'));
|
|
395
|
+
if (!Array.isArray(data.tools))
|
|
396
|
+
return null;
|
|
397
|
+
return data;
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function saveToolInventory(inv) {
|
|
404
|
+
try {
|
|
405
|
+
writeFileSync(TOOL_INVENTORY_FILE, JSON.stringify(inv, null, 2));
|
|
406
|
+
}
|
|
407
|
+
catch { /* non-fatal */ }
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Run a minimal SDK query with NO tool whitelist so the init message
|
|
411
|
+
* returns every tool Claude Code is actually surfacing — claude_ai_*
|
|
412
|
+
* connectors, plugins, built-ins, custom MCP servers. Cached for 24h.
|
|
413
|
+
* This removes any need to hardcode user-specific tool names in the
|
|
414
|
+
* system prompt; whatever Claude Desktop is currently connecting to the
|
|
415
|
+
* user's account becomes automatically available to the agent.
|
|
416
|
+
*/
|
|
417
|
+
export async function probeAvailableTools(force = false) {
|
|
418
|
+
const cached = loadToolInventory();
|
|
419
|
+
if (!force && cached) {
|
|
420
|
+
const age = Date.now() - Date.parse(cached.probedAt);
|
|
421
|
+
if (Number.isFinite(age) && age < TOOL_INVENTORY_MAX_AGE_MS)
|
|
422
|
+
return cached;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
426
|
+
const stream = query({
|
|
427
|
+
prompt: 'ok',
|
|
428
|
+
options: {
|
|
429
|
+
systemPrompt: 'Reply ok.',
|
|
430
|
+
model: 'claude-haiku-4-5',
|
|
431
|
+
permissionMode: 'bypassPermissions',
|
|
432
|
+
allowDangerouslySkipPermissions: true,
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
let tools = [];
|
|
436
|
+
for await (const msg of stream) {
|
|
437
|
+
if (msg?.type === 'system' && msg?.subtype === 'init' && Array.isArray(msg.tools)) {
|
|
438
|
+
tools = msg.tools;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
if (msg?.type === 'result')
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
const inv = { probedAt: new Date().toISOString(), tools };
|
|
445
|
+
saveToolInventory(inv);
|
|
446
|
+
logger.info({ toolCount: tools.length }, 'Tool inventory probed');
|
|
447
|
+
return inv;
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
logger.warn({ err }, 'Tool inventory probe failed — using cached or empty');
|
|
451
|
+
return cached ?? { probedAt: new Date(0).toISOString(), tools: [] };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
387
454
|
/**
|
|
388
455
|
* Register every integration found in a tool inventory. The SDK's system
|
|
389
456
|
* init message (subtype='init') includes a `tools: string[]` with the full
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Integration registry.
|
|
3
|
+
*
|
|
4
|
+
* Declarative metadata for every integration Clementine knows how to set up.
|
|
5
|
+
* The registry is the single source of truth for:
|
|
6
|
+
* - What env vars an integration needs
|
|
7
|
+
* - Where to get each credential (doc URLs)
|
|
8
|
+
* - Whether the integration is configured right now
|
|
9
|
+
* - How to surface gaps to the owner proactively
|
|
10
|
+
*
|
|
11
|
+
* Used by:
|
|
12
|
+
* - integration_status() — reports configured/partial/missing per integration
|
|
13
|
+
* - setup_integration() — walks the owner through setup conversationally
|
|
14
|
+
* - list_integrations() — enumerates what's available
|
|
15
|
+
* - System prompt — "Notion is configured, Stripe needs STRIPE_API_KEY"
|
|
16
|
+
*
|
|
17
|
+
* Replaces the previous pattern where the agent had to recall from chat
|
|
18
|
+
* history or invent which env vars a provider needs — the registry tells her
|
|
19
|
+
* exactly, with docs links, so she never has to guess.
|
|
20
|
+
*/
|
|
21
|
+
export type IntegrationKind = 'api-key' | 'oauth' | 'hybrid' | 'channel';
|
|
22
|
+
export interface IntegrationRequirement {
|
|
23
|
+
/** Env var name (uppercase, underscores). */
|
|
24
|
+
envVar: string;
|
|
25
|
+
/** One-line description shown during setup. */
|
|
26
|
+
label: string;
|
|
27
|
+
/** Whether this credential is required (false = optional). */
|
|
28
|
+
required: boolean;
|
|
29
|
+
/** Doc link where the owner can find or generate this credential. */
|
|
30
|
+
docUrl?: string;
|
|
31
|
+
/** Optional regex the value must match (for early validation). */
|
|
32
|
+
pattern?: RegExp;
|
|
33
|
+
/** If true, value is long-lived (API key). If false, short-lived (OAuth token). */
|
|
34
|
+
persistent?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface IntegrationDefinition {
|
|
37
|
+
/** Machine-readable slug (kebab-case). */
|
|
38
|
+
slug: string;
|
|
39
|
+
/** Human-friendly label. */
|
|
40
|
+
label: string;
|
|
41
|
+
/** One-line summary shown in status output. */
|
|
42
|
+
description: string;
|
|
43
|
+
/** Credential style. */
|
|
44
|
+
kind: IntegrationKind;
|
|
45
|
+
/** Required + optional env vars. */
|
|
46
|
+
requirements: IntegrationRequirement[];
|
|
47
|
+
/** Top-level docs URL. */
|
|
48
|
+
docUrl?: string;
|
|
49
|
+
/** Tools/capabilities this integration unlocks, for the status panel. */
|
|
50
|
+
capabilities?: string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The curated registry. Keep entries alphabetical within each kind for
|
|
54
|
+
* easier scanning. Adding a new integration: declare it here, and both
|
|
55
|
+
* integration_status and setup_integration automatically pick it up.
|
|
56
|
+
*/
|
|
57
|
+
export declare const INTEGRATIONS: IntegrationDefinition[];
|
|
58
|
+
export type IntegrationStatus = 'configured' | 'partial' | 'missing';
|
|
59
|
+
export interface IntegrationStatusReport {
|
|
60
|
+
slug: string;
|
|
61
|
+
label: string;
|
|
62
|
+
status: IntegrationStatus;
|
|
63
|
+
/** All required env vars that are currently set. */
|
|
64
|
+
have: string[];
|
|
65
|
+
/** Required env vars that are missing. */
|
|
66
|
+
missing: string[];
|
|
67
|
+
/** Optional env vars that are missing (informational only). */
|
|
68
|
+
optionalMissing: string[];
|
|
69
|
+
/** Helpful setup link. */
|
|
70
|
+
docUrl?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Classify each integration against the current environment.
|
|
74
|
+
* Pure function — caller passes in env so this is testable.
|
|
75
|
+
*/
|
|
76
|
+
export declare function classifyIntegrations(env: NodeJS.ProcessEnv, slugs?: string[]): IntegrationStatusReport[];
|
|
77
|
+
export declare function findIntegration(slug: string): IntegrationDefinition | undefined;
|
|
78
|
+
/** A short one-line summary for prompt injection. */
|
|
79
|
+
export declare function summarizeIntegrationStatus(env: NodeJS.ProcessEnv): string;
|
|
80
|
+
//# sourceMappingURL=integrations-registry.d.ts.map
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Integration registry.
|
|
3
|
+
*
|
|
4
|
+
* Declarative metadata for every integration Clementine knows how to set up.
|
|
5
|
+
* The registry is the single source of truth for:
|
|
6
|
+
* - What env vars an integration needs
|
|
7
|
+
* - Where to get each credential (doc URLs)
|
|
8
|
+
* - Whether the integration is configured right now
|
|
9
|
+
* - How to surface gaps to the owner proactively
|
|
10
|
+
*
|
|
11
|
+
* Used by:
|
|
12
|
+
* - integration_status() — reports configured/partial/missing per integration
|
|
13
|
+
* - setup_integration() — walks the owner through setup conversationally
|
|
14
|
+
* - list_integrations() — enumerates what's available
|
|
15
|
+
* - System prompt — "Notion is configured, Stripe needs STRIPE_API_KEY"
|
|
16
|
+
*
|
|
17
|
+
* Replaces the previous pattern where the agent had to recall from chat
|
|
18
|
+
* history or invent which env vars a provider needs — the registry tells her
|
|
19
|
+
* exactly, with docs links, so she never has to guess.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The curated registry. Keep entries alphabetical within each kind for
|
|
23
|
+
* easier scanning. Adding a new integration: declare it here, and both
|
|
24
|
+
* integration_status and setup_integration automatically pick it up.
|
|
25
|
+
*/
|
|
26
|
+
export const INTEGRATIONS = [
|
|
27
|
+
// NOTE: Anthropic auth is handled through `clementine login` (OAuth) or
|
|
28
|
+
// the ANTHROPIC_API_KEY env var — it's foundational and managed outside
|
|
29
|
+
// this registry because the auth path isn't uniform.
|
|
30
|
+
// ── Channels ──────────────────────────────────────────────────────
|
|
31
|
+
{
|
|
32
|
+
slug: 'discord',
|
|
33
|
+
label: 'Discord',
|
|
34
|
+
description: 'Main chat channel. Bot token + owner ID required.',
|
|
35
|
+
kind: 'channel',
|
|
36
|
+
docUrl: 'https://discord.com/developers/applications',
|
|
37
|
+
capabilities: ['chat', 'DMs', 'agent bots', 'notifications'],
|
|
38
|
+
requirements: [
|
|
39
|
+
{ envVar: 'DISCORD_TOKEN', label: 'Bot token', required: true, docUrl: 'https://discord.com/developers/applications', persistent: true },
|
|
40
|
+
{ envVar: 'DISCORD_OWNER_ID', label: 'Your Discord user ID (right-click your name → Copy User ID)', required: true, pattern: /^\d{15,22}$/, persistent: true },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
slug: 'slack',
|
|
45
|
+
label: 'Slack',
|
|
46
|
+
description: 'Alternate chat channel. Socket-mode app with bot + app tokens.',
|
|
47
|
+
kind: 'channel',
|
|
48
|
+
docUrl: 'https://api.slack.com/apps',
|
|
49
|
+
capabilities: ['chat', 'DMs', 'channel posts'],
|
|
50
|
+
requirements: [
|
|
51
|
+
{ envVar: 'SLACK_BOT_TOKEN', label: 'Bot token (xoxb-...)', required: true, docUrl: 'https://api.slack.com/apps', pattern: /^xoxb-/, persistent: true },
|
|
52
|
+
{ envVar: 'SLACK_APP_TOKEN', label: 'App-level token (xapp-...)', required: true, docUrl: 'https://api.slack.com/apps', pattern: /^xapp-/, persistent: true },
|
|
53
|
+
{ envVar: 'SLACK_OWNER_USER_ID', label: 'Your Slack user ID (starts with U)', required: false, pattern: /^U[A-Z0-9]{6,}$/, persistent: true },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
slug: 'telegram',
|
|
58
|
+
label: 'Telegram',
|
|
59
|
+
description: 'Chat channel via Bot API.',
|
|
60
|
+
kind: 'channel',
|
|
61
|
+
docUrl: 'https://core.telegram.org/bots',
|
|
62
|
+
capabilities: ['chat', 'DMs'],
|
|
63
|
+
requirements: [
|
|
64
|
+
{ envVar: 'TELEGRAM_BOT_TOKEN', label: 'Bot token from @BotFather', required: true, docUrl: 'https://t.me/BotFather', persistent: true },
|
|
65
|
+
{ envVar: 'TELEGRAM_OWNER_CHAT_ID', label: 'Your Telegram chat ID', required: false, pattern: /^-?\d+$/, persistent: true },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
slug: 'whatsapp',
|
|
70
|
+
label: 'WhatsApp (via Twilio)',
|
|
71
|
+
description: 'Chat channel via Twilio WhatsApp API.',
|
|
72
|
+
kind: 'channel',
|
|
73
|
+
docUrl: 'https://www.twilio.com/docs/whatsapp',
|
|
74
|
+
capabilities: ['chat', 'DMs'],
|
|
75
|
+
requirements: [
|
|
76
|
+
{ envVar: 'TWILIO_ACCOUNT_SID', label: 'Twilio account SID (AC...)', required: true, pattern: /^AC[a-f0-9]{32}$/i, persistent: true },
|
|
77
|
+
{ envVar: 'TWILIO_AUTH_TOKEN', label: 'Twilio auth token', required: true, persistent: true },
|
|
78
|
+
{ envVar: 'TWILIO_WHATSAPP_FROM', label: 'Twilio WhatsApp sender (whatsapp:+14155238886)', required: true, persistent: true },
|
|
79
|
+
{ envVar: 'WHATSAPP_OWNER_NUMBER', label: 'Your WhatsApp number (E.164 format, e.g. +15551234567)', required: true, pattern: /^\+\d{8,15}$/, persistent: true },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
// ── Productivity / knowledge ──────────────────────────────────────
|
|
83
|
+
{
|
|
84
|
+
slug: 'notion',
|
|
85
|
+
label: 'Notion',
|
|
86
|
+
description: 'Read/write Notion pages and databases via integration.',
|
|
87
|
+
kind: 'api-key',
|
|
88
|
+
docUrl: 'https://www.notion.so/my-integrations',
|
|
89
|
+
capabilities: ['docs', 'databases', 'task tracking'],
|
|
90
|
+
requirements: [
|
|
91
|
+
{ envVar: 'NOTION_API_KEY', label: 'Internal integration secret (secret_... or ntn_...)', required: true, docUrl: 'https://www.notion.so/my-integrations', persistent: true },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
slug: 'linear',
|
|
96
|
+
label: 'Linear',
|
|
97
|
+
description: 'Issue tracking and project management.',
|
|
98
|
+
kind: 'api-key',
|
|
99
|
+
docUrl: 'https://linear.app/settings/api',
|
|
100
|
+
capabilities: ['issues', 'projects', 'teams'],
|
|
101
|
+
requirements: [
|
|
102
|
+
{ envVar: 'LINEAR_API_KEY', label: 'Personal API key (lin_api_...)', required: true, docUrl: 'https://linear.app/settings/api', pattern: /^lin_api_/, persistent: true },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
slug: 'github',
|
|
107
|
+
label: 'GitHub',
|
|
108
|
+
description: 'Read PRs, issues, manage releases.',
|
|
109
|
+
kind: 'api-key',
|
|
110
|
+
docUrl: 'https://github.com/settings/tokens',
|
|
111
|
+
capabilities: ['pull requests', 'issues', 'releases'],
|
|
112
|
+
requirements: [
|
|
113
|
+
{ envVar: 'GITHUB_TOKEN', label: 'Personal access token (ghp_... or github_pat_...)', required: true, docUrl: 'https://github.com/settings/tokens?type=beta', pattern: /^(ghp_|github_pat_)/, persistent: true },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
// ── Email / calendar (OAuth) ──────────────────────────────────────
|
|
117
|
+
{
|
|
118
|
+
slug: 'microsoft-365',
|
|
119
|
+
label: 'Microsoft 365 (Outlook/Graph)',
|
|
120
|
+
description: 'Email, calendar, Teams, SharePoint via Microsoft Graph. OAuth app-only or delegated.',
|
|
121
|
+
kind: 'oauth',
|
|
122
|
+
docUrl: 'https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade',
|
|
123
|
+
capabilities: ['outlook inbox', 'outlook send', 'calendar', 'teams chat'],
|
|
124
|
+
requirements: [
|
|
125
|
+
{ envVar: 'MS_TENANT_ID', label: 'Azure AD tenant ID', required: true, pattern: /^[a-f0-9-]{36}$/i, persistent: true },
|
|
126
|
+
{ envVar: 'MS_CLIENT_ID', label: 'App registration client ID', required: true, pattern: /^[a-f0-9-]{36}$/i, persistent: true },
|
|
127
|
+
{ envVar: 'MS_CLIENT_SECRET', label: 'App registration client secret value', required: true, persistent: true },
|
|
128
|
+
{ envVar: 'MS_USER_EMAIL', label: 'Your primary email (used for delegated calls)', required: false, pattern: /@/, persistent: true },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
// ── CRM / sales ───────────────────────────────────────────────────
|
|
132
|
+
{
|
|
133
|
+
slug: 'salesforce',
|
|
134
|
+
label: 'Salesforce',
|
|
135
|
+
description: 'CRM access via SOAP/REST. Username + password + token flow.',
|
|
136
|
+
kind: 'api-key',
|
|
137
|
+
docUrl: 'https://help.salesforce.com/s/articleView?id=sf.code_sample_auth_api_oauth.htm',
|
|
138
|
+
capabilities: ['leads', 'contacts', 'opportunities', 'custom objects'],
|
|
139
|
+
requirements: [
|
|
140
|
+
{ envVar: 'SF_INSTANCE_URL', label: 'Salesforce instance URL (https://your-org.my.salesforce.com)', required: true, pattern: /^https?:\/\//, persistent: true },
|
|
141
|
+
{ envVar: 'SF_CLIENT_ID', label: 'Connected app consumer key', required: true, persistent: true },
|
|
142
|
+
{ envVar: 'SF_CLIENT_SECRET', label: 'Connected app consumer secret', required: true, persistent: true },
|
|
143
|
+
{ envVar: 'SF_USERNAME', label: 'Salesforce username (email)', required: true, pattern: /@/, persistent: true },
|
|
144
|
+
{ envVar: 'SF_PASSWORD', label: 'Salesforce password + security token concatenated', required: true, persistent: true },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
// ── AI auxiliaries ────────────────────────────────────────────────
|
|
148
|
+
{
|
|
149
|
+
slug: 'openai',
|
|
150
|
+
label: 'OpenAI',
|
|
151
|
+
description: 'GPT + Whisper (used for auxiliary tasks and fallback).',
|
|
152
|
+
kind: 'api-key',
|
|
153
|
+
docUrl: 'https://platform.openai.com/api-keys',
|
|
154
|
+
capabilities: ['transcription', 'image generation', 'fallback model'],
|
|
155
|
+
requirements: [
|
|
156
|
+
{ envVar: 'OPENAI_API_KEY', label: 'API key (sk-...)', required: true, docUrl: 'https://platform.openai.com/api-keys', pattern: /^sk-/, persistent: true },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
slug: 'elevenlabs',
|
|
161
|
+
label: 'ElevenLabs',
|
|
162
|
+
description: 'Text-to-speech for voice replies.',
|
|
163
|
+
kind: 'api-key',
|
|
164
|
+
docUrl: 'https://elevenlabs.io/app/settings/api-keys',
|
|
165
|
+
capabilities: ['voice synthesis'],
|
|
166
|
+
requirements: [
|
|
167
|
+
{ envVar: 'ELEVENLABS_API_KEY', label: 'API key', required: true, docUrl: 'https://elevenlabs.io/app/settings/api-keys', persistent: true },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
slug: 'groq',
|
|
172
|
+
label: 'Groq',
|
|
173
|
+
description: 'Fast inference, used for real-time transcription.',
|
|
174
|
+
kind: 'api-key',
|
|
175
|
+
docUrl: 'https://console.groq.com/keys',
|
|
176
|
+
capabilities: ['voice transcription'],
|
|
177
|
+
requirements: [
|
|
178
|
+
{ envVar: 'GROQ_API_KEY', label: 'API key (gsk_...)', required: true, docUrl: 'https://console.groq.com/keys', pattern: /^gsk_/, persistent: true },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
slug: 'google-ai',
|
|
183
|
+
label: 'Google AI (Gemini)',
|
|
184
|
+
description: 'Used for video analysis and multimodal tasks.',
|
|
185
|
+
kind: 'api-key',
|
|
186
|
+
docUrl: 'https://aistudio.google.com/apikey',
|
|
187
|
+
capabilities: ['video analysis', 'multimodal'],
|
|
188
|
+
requirements: [
|
|
189
|
+
{ envVar: 'GOOGLE_API_KEY', label: 'Gemini API key', required: true, docUrl: 'https://aistudio.google.com/apikey', persistent: true },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
// ── Payments ──────────────────────────────────────────────────────
|
|
193
|
+
{
|
|
194
|
+
slug: 'stripe',
|
|
195
|
+
label: 'Stripe',
|
|
196
|
+
description: 'Payments read/write (one-off or recurring).',
|
|
197
|
+
kind: 'api-key',
|
|
198
|
+
docUrl: 'https://dashboard.stripe.com/apikeys',
|
|
199
|
+
capabilities: ['customers', 'payments', 'subscriptions'],
|
|
200
|
+
requirements: [
|
|
201
|
+
{ envVar: 'STRIPE_SECRET_KEY', label: 'Secret key (sk_live_... or sk_test_...)', required: true, docUrl: 'https://dashboard.stripe.com/apikeys', pattern: /^sk_(live|test)_/, persistent: true },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
/**
|
|
206
|
+
* Classify each integration against the current environment.
|
|
207
|
+
* Pure function — caller passes in env so this is testable.
|
|
208
|
+
*/
|
|
209
|
+
export function classifyIntegrations(env, slugs) {
|
|
210
|
+
const targets = slugs?.length
|
|
211
|
+
? INTEGRATIONS.filter(i => slugs.includes(i.slug))
|
|
212
|
+
: INTEGRATIONS;
|
|
213
|
+
return targets.map(integration => {
|
|
214
|
+
const required = integration.requirements.filter(r => r.required);
|
|
215
|
+
const optional = integration.requirements.filter(r => !r.required);
|
|
216
|
+
const have = [];
|
|
217
|
+
const missing = [];
|
|
218
|
+
for (const req of required) {
|
|
219
|
+
if (env[req.envVar])
|
|
220
|
+
have.push(req.envVar);
|
|
221
|
+
else
|
|
222
|
+
missing.push(req.envVar);
|
|
223
|
+
}
|
|
224
|
+
const optionalMissing = optional
|
|
225
|
+
.filter(r => !env[r.envVar])
|
|
226
|
+
.map(r => r.envVar);
|
|
227
|
+
let status;
|
|
228
|
+
if (missing.length === 0 && have.length > 0)
|
|
229
|
+
status = 'configured';
|
|
230
|
+
else if (have.length > 0)
|
|
231
|
+
status = 'partial';
|
|
232
|
+
else
|
|
233
|
+
status = 'missing';
|
|
234
|
+
// Special case for hybrid: "have" counts if ANY required-or-alternative is set.
|
|
235
|
+
if (integration.kind === 'hybrid' && missing.length > 0 && integration.requirements.some(r => env[r.envVar])) {
|
|
236
|
+
status = 'configured';
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
slug: integration.slug,
|
|
240
|
+
label: integration.label,
|
|
241
|
+
status,
|
|
242
|
+
have,
|
|
243
|
+
missing,
|
|
244
|
+
optionalMissing,
|
|
245
|
+
docUrl: integration.docUrl,
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
export function findIntegration(slug) {
|
|
250
|
+
const normalized = slug.trim().toLowerCase();
|
|
251
|
+
return INTEGRATIONS.find(i => i.slug === normalized);
|
|
252
|
+
}
|
|
253
|
+
/** A short one-line summary for prompt injection. */
|
|
254
|
+
export function summarizeIntegrationStatus(env) {
|
|
255
|
+
const reports = classifyIntegrations(env);
|
|
256
|
+
const configured = reports.filter(r => r.status === 'configured').map(r => r.label);
|
|
257
|
+
const partial = reports.filter(r => r.status === 'partial').map(r => `${r.label} (missing ${r.missing.join(', ')})`);
|
|
258
|
+
const missing = reports.filter(r => r.status === 'missing').map(r => r.label);
|
|
259
|
+
const lines = [];
|
|
260
|
+
if (configured.length > 0)
|
|
261
|
+
lines.push(`**Configured:** ${configured.join(', ')}`);
|
|
262
|
+
if (partial.length > 0)
|
|
263
|
+
lines.push(`**Partial:** ${partial.join('; ')}`);
|
|
264
|
+
if (missing.length > 0)
|
|
265
|
+
lines.push(`**Available but not configured:** ${missing.join(', ')}`);
|
|
266
|
+
return lines.join('\n');
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=integrations-registry.js.map
|