clementine-agent 1.0.50 → 1.0.51

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.
@@ -1083,15 +1083,17 @@ If you're unsure what's happening first, run \`where_is_source\` — it reports
1083
1083
 
1084
1084
  ### Calling Claude Desktop connector tools (Drive, Gmail, etc.)
1085
1085
 
1086
- **Do not inspect \`claude-integrations.json\` or any integration file to decide whether a tool is "loaded."** That file is a telemetry log, not a source of truth. Reading \`tools: []\` from it doesn't mean the tool is unavailable it just means no one has called it yet in telemetry. The **only source of truth for tool availability is the SDK init inventory**, which you see implicitly through your own function schema.
1086
+ The **only source of truth for tool availability is your function schema**. Do not inspect \`claude-integrations.json\`, the inventory file, or run \`ToolSearch\` to "check" firstthose are telemetry caches, not reality.
1087
1087
 
1088
- The right sequence when the user asks you to do something with a connector:
1088
+ **The right sequence when the user asks you to do something with a connector:**
1089
1089
 
1090
- 1. **Just call the tool.** \`mcp__claude_ai_Google_Drive__search_files\`, \`mcp__claude_ai_Gmail__authenticate\`, etc. attempt the call. If it works, great. If it returns an auth error, report the auth error. If it returns results, use them.
1091
- 2. **Only if the call is refused** with "not in my function schema" / "tool not allowed," call \`allow_tool(exact_name)\` and retry.
1092
- 3. **Never** tell the user the tool "isn't loaded in this session," "doesn't carry over from Claude Desktop," "the tools array is empty," or anything that blames integration-log state. Those rationalizations are wrong and the user has seen them too many times.
1090
+ 1. **Just call the tool.** \`mcp__claude_ai_Google_Drive__search_files\`, \`mcp__claude_ai_Gmail__authenticate\`, etc. Attempt it. Report the literal result real data, auth error, or whatever.
1091
+ 2. **If refused** with "not in my function schema" / "tool not allowed," call \`allow_tool(exact_name)\` and retry. \`allow_tool\` auto-refreshes the inventory if the name is new — handles the case where the owner just added a connector at claude.ai.
1092
+ 3. **If the owner says "I just added X at claude.ai"** or anything similar, call \`refresh_tool_inventory\` first to pick up the new connector. Report what came online.
1093
1093
 
1094
- \`list_allowed_tools\` / \`disallow_tool\` manage the whitelist. Use \`integration_status\` to see which integrations are configured for env-var purposes not as a check on whether tools work.
1094
+ **Never** say the tool "isn't loaded in this session," "doesn't carry over from Claude Desktop," "the tools array is empty," or "MCP server still connecting." If any of those phrasings come to mind, call the tool directly and report what actually happens instead.
1095
+
1096
+ \`list_allowed_tools\` / \`disallow_tool\` manage the whitelist. \`integration_status\` is for env-var (API key) integrations — not for claude_ai_* connectors, which are schema-driven.
1095
1097
 
1096
1098
  ## Context Window Management
1097
1099
 
@@ -1402,6 +1404,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
1402
1404
  mcpTool('allow_tool'),
1403
1405
  mcpTool('list_allowed_tools'),
1404
1406
  mcpTool('disallow_tool'),
1407
+ mcpTool('refresh_tool_inventory'),
1405
1408
  mcpTool('self_restart'),
1406
1409
  mcpTool('self_update'),
1407
1410
  mcpTool('where_is_source'),
@@ -386,7 +386,10 @@ export function getClaudeIntegrations() {
386
386
  }
387
387
  // ── SDK tool inventory probe ───────────────────────────────────────────
388
388
  const TOOL_INVENTORY_FILE = path.join(BASE_DIR, '.tool-inventory.json');
389
- const TOOL_INVENTORY_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24h
389
+ // 1h TTL short enough that a connector added at claude.ai propagates
390
+ // within the hour without the user having to do anything. Forced refresh
391
+ // via probeAvailableTools(true) is still available.
392
+ const TOOL_INVENTORY_MAX_AGE_MS = 60 * 60 * 1000;
390
393
  export function loadToolInventory() {
391
394
  try {
392
395
  if (!existsSync(TOOL_INVENTORY_FILE))
package/dist/index.js CHANGED
@@ -545,10 +545,22 @@ async function asyncMain() {
545
545
  const { checkPermissionsOnStartup, bootstrapClaudeIntegrationsFromAuditLog, probeAvailableTools } = await import('./agent/mcp-bridge.js');
546
546
  checkPermissionsOnStartup();
547
547
  bootstrapClaudeIntegrationsFromAuditLog(path.join(config.BASE_DIR, 'logs', 'audit.log'));
548
- // Fire-and-forget: probe the SDK's full tool inventory so buildOptions
549
- // knows everything Claude Code is surfacing (claude_ai_* connectors,
550
- // plugins, etc.) without per-user hardcoding. Cached 24h.
551
- probeAvailableTools().catch(() => { });
548
+ // Probe the SDK's full tool inventory so buildOptions knows everything
549
+ // Claude Code is surfacing (claude_ai_* connectors, plugins, etc.)
550
+ // without per-user hardcoding. Cached 1h. On fresh probe, log a short
551
+ // summary so the owner can verify which connectors were detected
552
+ // without having to ask the assistant.
553
+ probeAvailableTools().then(inv => {
554
+ const integrations = new Set();
555
+ for (const t of inv.tools) {
556
+ const m = t.match(/^mcp__claude_ai_([^_]+(?:_[^_]+)*)__/);
557
+ if (m)
558
+ integrations.add(m[1].replace(/_/g, ' '));
559
+ }
560
+ if (integrations.size > 0) {
561
+ logger.info({ integrations: [...integrations].sort(), toolCount: inv.tools.length }, '🦞 Claude Desktop integrations detected');
562
+ }
563
+ }).catch(() => { });
552
564
  }
553
565
  catch { /* non-fatal */ }
554
566
  // ── Initialize layers ────────────────────────────────────────────
@@ -302,7 +302,7 @@ export function registerAdminTools(server) {
302
302
  const unique = [...new Set(tools)].sort();
303
303
  writeFileSync(ALLOWED_TOOLS_EXTRA, JSON.stringify(unique, null, 2));
304
304
  }
305
- server.tool('allow_tool', 'Add a tool name to your self-managed allowedTools list. Use when you see a tool in the SDK inventory but get "not in function schema" when you try to call it. Writes to ~/.clementine/allowed-tools-extra.json; takes effect on your NEXT query. Owner-DM only. Common case: Claude Desktop connector tools like mcp__claude_ai_Google_Drive__search_files that appear in the init inventory but aren\'t in your baseline whitelist.', {
305
+ server.tool('allow_tool', 'Add a tool name to your self-managed allowedTools list. Use when you see a tool in the SDK inventory but get "not in function schema" when you try to call it. Writes to ~/.clementine/allowed-tools-extra.json; takes effect on your NEXT query. If the tool name isn\'t yet in the cached inventory, this auto-refreshes the probe first — covers the case where the owner just added a new Claude Desktop connector. Owner-DM only.', {
306
306
  name: z.string().describe('Exact tool name (e.g. "mcp__claude_ai_Google_Drive__search_files")'),
307
307
  reason: z.string().optional().describe('Brief note: why you need this tool. For audit trail.'),
308
308
  }, async ({ name, reason }) => {
@@ -312,13 +312,28 @@ export function registerAdminTools(server) {
312
312
  const trimmed = name.trim();
313
313
  if (!trimmed)
314
314
  return textResult('Refused: empty tool name.');
315
- // Loose format check — MCP tool names, built-in tool names, or namespaced patterns.
316
315
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed.replace(/__[A-Za-z0-9_-]+/g, ''))) {
317
316
  return textResult(`Refused: "${trimmed}" doesn't look like a valid tool name. Use a literal name like "mcp__claude_ai_Google_Drive__search_files" or a built-in like "WebFetch".`);
318
317
  }
318
+ // Auto-refresh inventory when the requested tool isn't in the cached
319
+ // inventory. This handles the "owner just added a connector at
320
+ // claude.ai" case without requiring them to wait for the hourly
321
+ // refresh or restart the daemon.
322
+ let refreshNote = '';
323
+ try {
324
+ const { probeAvailableTools } = await import('../agent/mcp-bridge.js');
325
+ const cached = (await import('../agent/mcp-bridge.js')).loadToolInventory();
326
+ if (!cached?.tools?.includes(trimmed)) {
327
+ const refreshed = await probeAvailableTools(true);
328
+ if (refreshed.tools.includes(trimmed)) {
329
+ refreshNote = ' (picked up from a fresh probe — looks like the connector just came online)';
330
+ }
331
+ }
332
+ }
333
+ catch { /* non-fatal */ }
319
334
  const current = readExtraAllowedTools();
320
335
  if (current.includes(trimmed)) {
321
- return textResult(`${trimmed} is already in your extra-allowed list. If it's still being refused, it may be disallowed by a higher-priority block (heartbeat/cron disallowed tools, profile tier).`);
336
+ return textResult(`${trimmed} is already in your extra-allowed list${refreshNote}. If it's still being refused, it may be disallowed by a higher-priority block (heartbeat/cron disallowed tools, profile tier).`);
322
337
  }
323
338
  current.push(trimmed);
324
339
  try {
@@ -328,7 +343,32 @@ export function registerAdminTools(server) {
328
343
  return textResult(`Failed to persist: ${String(err).slice(0, 200)}`);
329
344
  }
330
345
  logger.info({ name: trimmed, reason, totalExtras: current.length }, 'allow_tool');
331
- return textResult(`Added ${trimmed} to ~/.clementine/allowed-tools-extra.json (${current.length} total extras). Active on your next query — no daemon restart needed.${reason ? ` Reason: ${reason}` : ''}`);
346
+ return textResult(`Added ${trimmed} to ~/.clementine/allowed-tools-extra.json (${current.length} total extras)${refreshNote}. Active on your next query — no daemon restart needed.${reason ? ` Reason: ${reason}` : ''}`);
347
+ });
348
+ server.tool('refresh_tool_inventory', 'Force a fresh probe of the SDK\'s tool inventory, picking up any Claude Desktop connectors the owner has added since the last cache refresh. Owner-DM only. Use this when the owner says "I just added X at claude.ai" or when an expected integration isn\'t showing up. Updates ~/.clementine/.tool-inventory.json and syncs claude-integrations.json. Returns a diff of what changed.', {}, async () => {
349
+ const gate = requireOwnerDm();
350
+ if (!gate.ok)
351
+ return textResult(gate.message);
352
+ try {
353
+ const { probeAvailableTools, loadToolInventory } = await import('../agent/mcp-bridge.js');
354
+ const before = loadToolInventory();
355
+ const beforeSet = new Set(before?.tools ?? []);
356
+ const after = await probeAvailableTools(true);
357
+ const afterSet = new Set(after.tools);
358
+ const added = [...afterSet].filter(t => !beforeSet.has(t));
359
+ const removed = [...beforeSet].filter(t => !afterSet.has(t));
360
+ const lines = [`Probed ${after.tools.length} tools.`];
361
+ if (added.length)
362
+ lines.push(`\n**Added since last probe:** ${added.length}\n${added.slice(0, 20).map(t => `- ${t}`).join('\n')}${added.length > 20 ? `\n... +${added.length - 20} more` : ''}`);
363
+ if (removed.length)
364
+ lines.push(`\n**Removed since last probe:** ${removed.length}\n${removed.slice(0, 10).map(t => `- ${t}`).join('\n')}`);
365
+ if (!added.length && !removed.length)
366
+ lines.push('No change since last probe.');
367
+ return textResult(lines.join('\n'));
368
+ }
369
+ catch (err) {
370
+ return textResult(`Probe failed: ${String(err).slice(0, 200)}`);
371
+ }
332
372
  });
333
373
  server.tool('list_allowed_tools', 'Show the current self-managed allowedTools extras (tools you added via allow_tool on top of the built-in whitelist). Owner-DM only.', {}, async () => {
334
374
  const gate = requireOwnerDm();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.50",
3
+ "version": "1.0.51",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",