clementine-agent 1.0.43 → 1.0.45

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.
@@ -1058,13 +1058,20 @@ When ${owner} gives you an API key, access token, or similar credential in chat,
1058
1058
 
1059
1059
  Use \`env_list\` to show what's configured (values masked) and \`env_unset\` to remove one. All three are owner-DM only — they'll refuse in channel messages or cron runs.
1060
1060
 
1061
- ### Tool schema self-extension
1061
+ ### When a tool call is refused
1062
1062
 
1063
- If a tool call fails with **"not in my function schema" / "tool not allowed" / "unknown tool"** BUT you can see it in the SDK init inventory (e.g. \`mcp__claude_ai_Google_Drive__search_files\`), call \`allow_tool(name)\` to add it to your whitelist. Takes effect on your next query — no restart, no owner intervention. Then try the original task again.
1063
+ If any tool call fails with "not in my function schema" / "tool not allowed" / "unknown tool" while you can see the tool exists in your SDK inventory:
1064
1064
 
1065
- This is the correct response to "tool not in schema" errors. **Do not dead-end** and tell ${owner} to edit a config file. Use \`list_allowed_tools\` to see what you've already added, \`disallow_tool\` to remove one.
1065
+ 1. Call \`allow_tool("<exact_tool_name>")\` persists to your own whitelist
1066
+ 2. Retry the original call
1066
1067
 
1067
- Don't tell ${owner} "add this to your .env" — just call env_set and report what you saved. For integrations connected at claude.ai (Google Drive, Gmail, Slack, Notion, Linear, etc.), the \`mcp__claude_ai_*\` tools appear in your SDK init inventory; if your first call is refused, call \`allow_tool\` with the exact tool name and retry.
1068
+ Takes effect on your next query. No restart, no config editing, no asking ${owner} to help.
1069
+
1070
+ **Never** respond with "this tool isn't available to agents" or "you need to edit my config" or "try it in a different app instead." If the tool name appears in your inventory, you can add it. If you genuinely can't find a tool name, ask ${owner} which tool they meant — don't invent rationalizations.
1071
+
1072
+ \`list_allowed_tools\` shows what you've added. \`disallow_tool\` removes one.
1073
+
1074
+ For \`.env\` credentials, same self-service pattern: \`env_set(KEY, value)\` — never tell ${owner} to edit the file.
1068
1075
 
1069
1076
  ## Context Window Management
1070
1077
 
@@ -1079,40 +1086,21 @@ Delegate data-heavy work (SEO, analytics, bulk API calls for 3+ entities) to sub
1079
1086
  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.
1080
1087
  `);
1081
1088
  }
1082
- // Inject Claude Desktop integration awareness. Two lists:
1083
- // - Known-confirmed: integrations the agent has actually used before
1084
- // (persisted in claude-integrations.json). High confidence.
1085
- // - Possibly-available: common integrations the owner may have connected
1086
- // at claude.ai level that we don't have a usage record for yet. The
1087
- // integrations-file is written reactively — only entries with
1088
- // successful tool uses get captured — so a freshly-connected
1089
- // Google Drive / Gmail / Slack connector is invisible to us until we
1090
- // try it. Hint the agent that it can try these blindly; the tool
1091
- // call will either succeed or return an auth error, at which point
1092
- // the record gets captured.
1089
+ // Inject Claude Desktop integration awareness. Derived from the probed
1090
+ // SDK tool inventory — whatever the current user has connected at the
1091
+ // claude.ai level shows up here automatically, no per-install hardcoding.
1093
1092
  try {
1094
- const integrations = _mcpBridge?.getClaudeIntegrations() ?? [];
1095
- const knownNames = new Set(integrations.map(ig => ig.name));
1096
- if (integrations.length > 0) {
1097
- const names = integrations.map(ig => ig.label).join(', ');
1098
- parts.push(`**Connected via Claude Desktop:** ${names}. Use their \`mcp__claude_ai_*\` tools (e.g. \`mcp__claude_ai_Google_Drive__search_files\`).`);
1099
- }
1100
- // Common integrations to speculatively mention. If the owner has
1101
- // connected any of these at claude.ai, the corresponding tool names
1102
- // will work even if no prior record exists.
1103
- const SPECULATIVE = [
1104
- ['Google_Drive', 'Google Drive'], ['Gmail', 'Gmail'],
1105
- ['Google_Calendar', 'Google Calendar'], ['Google_Workspace', 'Google Workspace'],
1106
- ['Slack', 'Slack'], ['Notion', 'Notion'], ['GitHub', 'GitHub'],
1107
- ['Linear', 'Linear'], ['Asana', 'Asana'], ['Jira', 'Jira'],
1108
- ['Dropbox', 'Dropbox'], ['Salesforce', 'Salesforce'],
1109
- ['Microsoft_365', 'Microsoft 365'],
1110
- ];
1111
- const maybe = SPECULATIVE.filter(([name]) => !knownNames.has(name)).map(([, label]) => label);
1112
- if (maybe.length > 0) {
1113
- parts.push(`**Possibly connected (try them — they'll either work or return an auth error):** ${maybe.join(', ')}. ` +
1114
- `Tool names follow \`mcp__claude_ai_<IntegrationName>__<tool>\` — e.g. \`mcp__claude_ai_Google_Drive__search_files\`, \`mcp__claude_ai_Gmail__authenticate\`. ` +
1115
- `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.`);
1093
+ const inv = _mcpBridge?.loadToolInventory();
1094
+ const labels = new Set();
1095
+ if (inv?.tools) {
1096
+ for (const t of inv.tools) {
1097
+ const m = t.match(/^mcp__claude_ai_([^_]+(?:_[^_]+)*)__/);
1098
+ if (m)
1099
+ labels.add(m[1].replace(/_/g, ' '));
1100
+ }
1101
+ }
1102
+ if (labels.size > 0) {
1103
+ 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.`);
1116
1104
  }
1117
1105
  }
1118
1106
  catch { /* non-fatal */ }
@@ -1434,28 +1422,26 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
1434
1422
  }
1435
1423
  }
1436
1424
  catch { /* non-fatal — dynamic tools are supplementary */ }
1437
- // Claude Desktop connector tools (mcp__claude_ai_*). These reach the SDK
1438
- // subprocess via Claude Code's runtime but are NOT added to allowedTools
1439
- // automatically which caused the model to see them in the init
1440
- // inventory but get refused when it tried to call one ("not in my
1441
- // function schema"). Add every tool from every auto-registered
1442
- // integration (populated from the SDK init message on prior queries) so
1443
- // the whitelist matches reality.
1425
+ // Merge the SDK's probed tool inventory. On daemon startup we run a
1426
+ // one-shot query with no whitelist to discover every tool Claude Code
1427
+ // surfaces (mcp__claude_ai_* connectors, plugins, custom MCP servers,
1428
+ // built-ins). The inventory is cached 24h. This makes the whitelist
1429
+ // match whatever the *current user* has connected — no per-install
1430
+ // hardcoding of tool names in the system prompt or code.
1444
1431
  try {
1445
- const integrations = _mcpBridge?.getClaudeIntegrations() ?? [];
1446
- for (const ig of integrations) {
1447
- for (const tool of ig.tools) {
1448
- const fullName = `mcp__claude_ai_${ig.name}__${tool}`;
1449
- if (!allowedTools.includes(fullName))
1450
- allowedTools.push(fullName);
1432
+ const inv = _mcpBridge?.loadToolInventory();
1433
+ if (inv && Array.isArray(inv.tools)) {
1434
+ for (const t of inv.tools) {
1435
+ if (typeof t === 'string' && !allowedTools.includes(t))
1436
+ allowedTools.push(t);
1451
1437
  }
1452
1438
  }
1453
1439
  }
1454
1440
  catch { /* non-fatal */ }
1455
1441
  // Self-service extension — Clementine can add tools to her own whitelist
1456
1442
  // at runtime via the `allow_tool` MCP tool, writing to allowed-tools-
1457
- // extra.json. This eliminates the "tool not in schema dead end → tell
1458
- // owner to edit config" failure pattern. See admin-tools.ts:allow_tool.
1443
+ // extra.json. Covers the case where a user connects a new integration
1444
+ // between daemon startup probes (< 24h window).
1459
1445
  try {
1460
1446
  const extraPath = path.join(BASE_DIR, 'allowed-tools-extra.json');
1461
1447
  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
@@ -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
package/dist/index.js CHANGED
@@ -536,9 +536,13 @@ async function asyncMain() {
536
536
  }
537
537
  // ── Check MCP extension permissions ────────────────────────────
538
538
  try {
539
- const { checkPermissionsOnStartup, bootstrapClaudeIntegrationsFromAuditLog } = await import('./agent/mcp-bridge.js');
539
+ const { checkPermissionsOnStartup, bootstrapClaudeIntegrationsFromAuditLog, probeAvailableTools } = await import('./agent/mcp-bridge.js');
540
540
  checkPermissionsOnStartup();
541
541
  bootstrapClaudeIntegrationsFromAuditLog(path.join(config.BASE_DIR, 'logs', 'audit.log'));
542
+ // Fire-and-forget: probe the SDK's full tool inventory so buildOptions
543
+ // knows everything Claude Code is surfacing (claude_ai_* connectors,
544
+ // plugins, etc.) without per-user hardcoding. Cached 24h.
545
+ probeAvailableTools().catch(() => { });
542
546
  }
543
547
  catch { /* non-fatal */ }
544
548
  // ── Initialize layers ────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",