clementine-agent 1.18.12 → 1.18.13

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.
@@ -126,6 +126,15 @@ export const TOOL_BUNDLES = [
126
126
  function uniqueStrings(values) {
127
127
  return [...new Set([...values].filter((v) => !!v && v.trim().length > 0))];
128
128
  }
129
+ function explicitMcpServers(scopeText) {
130
+ const servers = new Set();
131
+ const re = /\bmcp__([A-Za-z0-9_-]+)__[A-Za-z0-9_.:-]+\b/g;
132
+ let match;
133
+ while ((match = re.exec(scopeText)) !== null) {
134
+ servers.add(match[1]);
135
+ }
136
+ return uniqueStrings(servers);
137
+ }
129
138
  export function routeToolSurface(text) {
130
139
  const scopeText = text?.trim() ?? '';
131
140
  if (!scopeText) {
@@ -162,13 +171,26 @@ export function routeToolSurface(text) {
162
171
  composio.add(slug);
163
172
  inheritFullClaudeEnv = inheritFullClaudeEnv || bundle.inheritFullClaudeEnv === true;
164
173
  }
174
+ for (const server of explicitMcpServers(scopeText)) {
175
+ if (server.startsWith('claude_ai_')) {
176
+ external.add(server.slice('claude_ai_'.length));
177
+ }
178
+ else {
179
+ // Exact `mcp__<server>__<tool>` mentions are authoritative. Add the
180
+ // name as both a direct MCP server and a Composio toolkit; whichever
181
+ // source is actually connected will mount, and the other path no-ops.
182
+ external.add(server);
183
+ composio.add(server);
184
+ }
185
+ inheritFullClaudeEnv = true;
186
+ }
165
187
  return {
166
188
  bundles: uniqueStrings(bundles),
167
189
  externalMcpServers: uniqueStrings(external),
168
190
  composioToolkits: uniqueStrings(composio),
169
191
  inheritFullClaudeEnv,
170
192
  fullSurface: false,
171
- reason: bundles.size > 0 ? 'matched' : 'empty',
193
+ reason: bundles.size > 0 || external.size > 0 || composio.size > 0 ? 'matched' : 'empty',
172
194
  };
173
195
  }
174
196
  //# sourceMappingURL=tool-router.js.map
@@ -59,7 +59,7 @@ export interface ConnectorRecipe {
59
59
  description: string;
60
60
  /** Emoji shown next to the label. */
61
61
  icon: string;
62
- /** Matches the key in ~/.clementine/claude-integrations.json */
62
+ /** Matches the tool source name; "*" recipes are offered for every source. */
63
63
  integration: string;
64
64
  /** Tools we rely on for this recipe. Used only to warn if the integration
65
65
  * hasn't surfaced them yet in claude-integrations.json. */
@@ -25,6 +25,10 @@ function slugify(s) {
25
25
  .replace(/^-+|-+$/g, '')
26
26
  .slice(0, 40) || 'feed';
27
27
  }
28
+ function inferToolServer(toolName) {
29
+ const match = String(toolName).match(/^mcp__([^_]+(?:_[^_]+)*)__/);
30
+ return match?.[1] ?? 'tool';
31
+ }
28
32
  const COMMIT_INSTRUCTIONS = `When you have the records collected, call the \`brain_ingest_folder\` MCP tool with:
29
33
  - \`slug\`: "{{slug}}"
30
34
  - \`records\`: an array of \`{title, externalId, content, metadata}\` objects (one per item). \`externalId\` should be the source provider's stable id so re-runs dedup. \`metadata\` can include any fields you want preserved (url, modifiedAt, author).
@@ -35,6 +39,102 @@ If the tool returns an error, include the error text in your summary.`;
35
39
  const MEMORY_DELTA_INSTRUCTIONS = `Before committing, call \`memory_recall\` for the feed slug/topic and use the returned chunks as the current memory state for this source. Keep records that are new, materially changed, or contain a new finding. Drop exact duplicates and rows that add no useful information. The ingestion pipeline will write markdown and embeddings; do not call \`memory_write\` for these feed records.`;
36
40
  // ── Recipes ────────────────────────────────────────────────────────────
37
41
  export const RECIPES = [
42
+ {
43
+ id: 'tool-backed-memory-seed',
44
+ label: 'Any tool: call and seed memory',
45
+ description: 'Call a selected tool from this connector, compare results with current memory, and ingest new or changed findings.',
46
+ icon: '🔌',
47
+ integration: '*',
48
+ requiredTools: [],
49
+ fields: [
50
+ {
51
+ key: 'topic',
52
+ label: 'Memory topic',
53
+ placeholder: 'customers, calls, leads, deals, meetings...',
54
+ required: true,
55
+ help: 'Used for recall, deduping, and the generated feed slug.',
56
+ },
57
+ {
58
+ key: 'toolName',
59
+ label: 'Tool to call',
60
+ required: true,
61
+ help: 'Pick the exact tool this feed should call when it runs.',
62
+ },
63
+ {
64
+ key: 'callGoal',
65
+ label: 'What to pull',
66
+ placeholder: 'Fetch updated HubSpot contacts modified since the last run...',
67
+ required: true,
68
+ help: 'Describe the records to fetch, filters to apply, and any pagination bounds.',
69
+ },
70
+ {
71
+ key: 'variablesJson',
72
+ label: 'Variables JSON',
73
+ placeholder: '{"listId":"123","limit":100,"updatedAfter":"last_run"}',
74
+ help: 'Optional arguments, IDs, ranges, filters, or query variables the tool should use.',
75
+ },
76
+ {
77
+ key: 'recordStrategy',
78
+ label: 'Record strategy',
79
+ placeholder: 'One record per contact. Use email as stable id. Summarize lifecycle stage, owner, last activity, and new changes.',
80
+ help: 'Tell the agent how to convert the tool output into memory records.',
81
+ },
82
+ {
83
+ key: 'slug',
84
+ label: 'Slug override',
85
+ placeholder: 'hubspot-contacts',
86
+ help: 'Optional. Leave blank to derive one from the connector and topic.',
87
+ },
88
+ {
89
+ key: 'limit',
90
+ label: 'Max records per run',
91
+ placeholder: '100',
92
+ defaultValue: '100',
93
+ },
94
+ ],
95
+ defaultSchedule: '0 8 * * *',
96
+ tier: 2,
97
+ slugFromValues: (v) => `tool-${slugify(v.slug || `${v.toolSourceName || inferToolServer(v.toolName || '')}-${v.topic || v.toolName || 'feed'}`)}`,
98
+ buildPrompt: (v, ctx) => {
99
+ const sourceName = v.toolSourceName || inferToolServer(v.toolName || '');
100
+ const sourceKind = v.toolSourceKind || 'mcp';
101
+ const sourceLabel = v.toolSourceLabel || sourceName;
102
+ const topic = v.topic || 'tool-backed memory';
103
+ const limit = v.limit || '100';
104
+ return `You are running a generic tool-backed memory seed feed.
105
+
106
+ Tool source:
107
+ - Label: "${sourceLabel}"
108
+ - Source name: "${sourceName}"
109
+ - Source kind: "${sourceKind}"
110
+ - Tool: \`${v.toolName}\`
111
+
112
+ Goal: ${v.callGoal || `Call ${v.toolName} and ingest useful returned data into memory.`}
113
+
114
+ Variables JSON:
115
+ \`\`\`json
116
+ ${(v.variablesJson || '{}').trim() || '{}'}
117
+ \`\`\`
118
+
119
+ Record strategy:
120
+ ${v.recordStrategy || 'Convert the tool response into one memory record per returned entity or event. Use the provider stable id when available; otherwise use a deterministic hash of the source, topic, and meaningful record key.'}
121
+
122
+ Steps:
123
+ 1. Call exactly this selected tool: \`${v.toolName}\`. Use the Variables JSON and the Goal above as the tool-call inputs. If the tool schema needs differently named arguments, map the provided variables to that schema. Do not switch to a different external tool unless this tool returns a clear instruction that another tool is required to read the selected records.
124
+ 2. If the tool supports pagination or modified-since filters, prefer new/updated records and stop after ${limit} records. If no modified-since filter is available, fetch the most relevant ${limit} records.
125
+ 3. Normalize the tool result into candidate records. Preserve stable ids, URLs, timestamps, owners/authors, status fields, and provider metadata. Skip empty or purely administrative records.
126
+ 4. ${MEMORY_DELTA_INSTRUCTIONS}
127
+ Use this recall query: \`source:${ctx.slug} ${topic} ${sourceLabel} ${v.toolName}\`.
128
+ 5. Compare the normalized candidates with recalled memory. Keep only candidates that are new, materially changed, or produce a new useful finding. Drop exact duplicates and trivial timestamp-only changes unless the timestamp itself is the useful fact.
129
+ 6. For each kept candidate, build one record:
130
+ - \`title\`: a compact human label including the topic and record name/id.
131
+ - \`externalId\`: \`${sourceName}:${topic}:<providerStableIdOrDeterministicHash>\`.
132
+ - \`content\`: markdown containing the current facts, the new/changed finding, and a "Source data" section with relevant returned fields.
133
+ - \`metadata\`: \`{provider:"${sourceName}", toolSource:"${sourceKind}", toolName:"${v.toolName}", topic:"${topic}", fetchedAt, sourceUrl, updatedAt}\` plus any provider-specific keys worth preserving.
134
+ 7. ${COMMIT_INSTRUCTIONS.replace(/{{slug}}/g, ctx.slug).replace(/{{targetFolder}}/g, ctx.targetFolder)}
135
+ `;
136
+ },
137
+ },
38
138
  {
39
139
  id: 'gdrive-watch-folder',
40
140
  label: 'Google Drive: watch a folder',
@@ -3890,7 +3890,7 @@ export async function cmdDashboard(opts) {
3890
3890
  const connected = await composio.listConnectedToolkits();
3891
3891
  const activeSlugs = [...new Set(connected
3892
3892
  .filter((c) => c.status === 'ACTIVE')
3893
- .filter((c) => recipeIntegrations.has(c.slug))
3893
+ .filter((c) => recipeIntegrations.has('*') || recipeIntegrations.has(c.slug))
3894
3894
  .map((c) => c.slug))];
3895
3895
  if (activeSlugs.length) {
3896
3896
  const { listComposioToolkitTools } = await import('../integrations/composio/mcp-bridge.js');
@@ -14115,6 +14115,11 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14115
14115
  for (const f of (s.recipe.fields || [])) {
14116
14116
  if (f.defaultValue) s.values[f.key] = f.defaultValue;
14117
14117
  }
14118
+ if (s.recipe.integration === '*' && s.pick) {
14119
+ s.values.toolSourceName = s.pick.name;
14120
+ s.values.toolSourceKind = s.pick.kind;
14121
+ s.values.toolSourceLabel = s.pick.label;
14122
+ }
14118
14123
  s.schedule = s.recipe.defaultSchedule;
14119
14124
  s.step = 2;
14120
14125
  } else if (s.step === 2) {
@@ -14290,6 +14295,13 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14290
14295
  if (field) await brainRenderFieldPicker(field, s.values);
14291
14296
  }
14292
14297
 
14298
+ function brainFullToolNameForPick(pick, tool) {
14299
+ if (!pick || !tool) return tool || '';
14300
+ if (String(tool).startsWith('mcp__')) return tool;
14301
+ const server = pick.kind === 'claude-desktop' ? ('claude_ai_' + pick.name) : pick.name;
14302
+ return 'mcp__' + server + '__' + tool;
14303
+ }
14304
+
14293
14305
  function brainFeedWizardRender() {
14294
14306
  if (!brainFeedWizardState) return;
14295
14307
  const s = brainFeedWizardState;
@@ -14317,7 +14329,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14317
14329
  }).join('') + '</div>';
14318
14330
  }
14319
14331
  } else if (s.step === 1) {
14320
- const recipes = (s.catalog.recipes || []).filter(function(r) { return r.integration === s.pick.name; });
14332
+ const recipes = (s.catalog.recipes || []).filter(function(r) { return r.integration === s.pick.name || r.integration === '*'; });
14321
14333
  if (!recipes.length) {
14322
14334
  html = '<div style="color:var(--muted)">No recipes for this connector yet.</div>';
14323
14335
  } else {
@@ -14346,6 +14358,25 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14346
14358
  '<div style="color:var(--muted);font-size:13px;padding:6px">Loading choices…</div>' +
14347
14359
  '</div>' +
14348
14360
  '<input type="hidden" data-field="' + f.key + '" value="' + escapeHtml(val) + '">';
14361
+ } else if (s.recipe.integration === '*' && f.key === 'toolName') {
14362
+ const tools = (s.pick && s.pick.tools) || [];
14363
+ if (!tools.length) {
14364
+ control = '<input type="text" data-field="' + f.key + '" value="' + escapeHtml(val) + '" placeholder="mcp__server__TOOL_NAME" style="width:100%">';
14365
+ } else {
14366
+ const options = tools.map(function(t) {
14367
+ const full = brainFullToolNameForPick(s.pick, t);
14368
+ const selected = full === val ? ' selected' : '';
14369
+ return '<option value="' + escapeHtml(full) + '"' + selected + '>' + escapeHtml(t) + '</option>';
14370
+ }).join('');
14371
+ control = '<select data-field="' + f.key + '" style="width:100%;padding:6px">' +
14372
+ '<option value="">— pick a tool —</option>' +
14373
+ options +
14374
+ '</select>' +
14375
+ '<div style="font-size:11px;color:var(--muted);margin-top:4px">The feed will call the selected tool exactly, then compare returned records with memory.</div>';
14376
+ }
14377
+ } else if (s.recipe.integration === '*' && ['callGoal', 'variablesJson', 'recordStrategy'].includes(f.key)) {
14378
+ const minHeight = f.key === 'variablesJson' ? '70px' : '92px';
14379
+ control = '<textarea data-field="' + f.key + '" placeholder="' + escapeHtml(f.placeholder || '') + '" style="width:100%;min-height:' + minHeight + ';resize:vertical">' + escapeHtml(val) + '</textarea>';
14349
14380
  } else {
14350
14381
  control = '<input type="text" data-field="' + f.key + '" value="' + escapeHtml(val) + '" placeholder="' + escapeHtml(f.placeholder || '') + '" style="width:100%">';
14351
14382
  }
@@ -14387,6 +14418,11 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
14387
14418
  function brainFeedWizardPickRecipe(id) {
14388
14419
  const r = (brainFeedWizardState.catalog.recipes || []).find(function(x) { return x.id === id; });
14389
14420
  brainFeedWizardState.recipe = r;
14421
+ if (r && r.integration === '*' && brainFeedWizardState.pick) {
14422
+ brainFeedWizardState.values.toolSourceName = brainFeedWizardState.pick.name;
14423
+ brainFeedWizardState.values.toolSourceKind = brainFeedWizardState.pick.kind;
14424
+ brainFeedWizardState.values.toolSourceLabel = brainFeedWizardState.pick.label;
14425
+ }
14390
14426
  brainFeedWizardRender();
14391
14427
  }
14392
14428
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.12",
3
+ "version": "1.18.13",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",