clementine-agent 1.18.51 → 1.18.53

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.
@@ -41,18 +41,18 @@ export interface ClaudeIntegration {
41
41
  connected: boolean;
42
42
  }
43
43
  /**
44
- * Check if a tool name is a Claude Desktop integration tool.
44
+ * Check if a tool name is a claude.ai integration tool.
45
45
  * Format: mcp__claude_ai_<IntegrationName>__<tool_name>
46
46
  */
47
47
  export declare function isClaudeDesktopTool(toolName: string): boolean;
48
48
  /** Load persisted integrations from disk. */
49
49
  export declare function loadClaudeIntegrations(): Record<string, ClaudeIntegration>;
50
50
  /**
51
- * Record a Claude Desktop integration tool use.
51
+ * Record a claude.ai integration tool use.
52
52
  * Call this whenever a mcp__claude_ai_* tool is seen in a tool_use block.
53
53
  */
54
54
  export declare function recordClaudeIntegrationUse(toolName: string): void;
55
- /** Get all discovered Claude Desktop integrations as a list. */
55
+ /** Get all discovered claude.ai integrations as a list. */
56
56
  export declare function getClaudeIntegrations(): ClaudeIntegration[];
57
57
  export interface ToolInventory {
58
58
  /** ISO timestamp of the probe */
@@ -66,17 +66,18 @@ export declare function loadToolInventory(): ToolInventory | null;
66
66
  * returns every tool Claude Code is actually surfacing — claude_ai_*
67
67
  * connectors, plugins, built-ins, custom MCP servers. Cached for 24h.
68
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.
69
+ * system prompt; whatever the SDK currently surfaces (claude.ai
70
+ * connectors, Composio toolkits, local stdio MCP servers) becomes
71
+ * automatically available to the agent.
71
72
  */
72
73
  export declare function probeAvailableTools(force?: boolean): Promise<ToolInventory>;
73
74
  /**
74
75
  * Register every integration found in a tool inventory. The SDK's system
75
76
  * init message (subtype='init') includes a `tools: string[]` with the full
76
77
  * set of tools the agent actually has access to this session — including
77
- * every mcp__claude_ai_* tool Claude Desktop is surfacing. Walking that
78
- * list on init gives us the authoritative, up-to-date integration set
79
- * without waiting for the agent to blindly try each one.
78
+ * every mcp__claude_ai_* tool the SDK is surfacing from claude.ai
79
+ * connectors. Walking that list on init gives us the authoritative,
80
+ * up-to-date integration set without waiting for the agent to blindly try each one.
80
81
  *
81
82
  * Idempotent: if an entry already exists, we merge new tool names into it
82
83
  * and bump `connected = true` without touching firstSeen/lastUsed.
@@ -318,7 +318,7 @@ const INTEGRATION_LABELS = {
318
318
  'Salesforce': 'Salesforce',
319
319
  };
320
320
  /**
321
- * Check if a tool name is a Claude Desktop integration tool.
321
+ * Check if a tool name is a claude.ai integration tool.
322
322
  * Format: mcp__claude_ai_<IntegrationName>__<tool_name>
323
323
  */
324
324
  export function isClaudeDesktopTool(toolName) {
@@ -351,7 +351,7 @@ function saveClaudeIntegrations(integrations) {
351
351
  writeFileSync(INTEGRATIONS_FILE, JSON.stringify(integrations, null, 2));
352
352
  }
353
353
  /**
354
- * Record a Claude Desktop integration tool use.
354
+ * Record a claude.ai integration tool use.
355
355
  * Call this whenever a mcp__claude_ai_* tool is seen in a tool_use block.
356
356
  */
357
357
  export function recordClaudeIntegrationUse(toolName) {
@@ -380,7 +380,7 @@ export function recordClaudeIntegrationUse(toolName) {
380
380
  }
381
381
  saveClaudeIntegrations(integrations);
382
382
  }
383
- /** Get all discovered Claude Desktop integrations as a list. */
383
+ /** Get all discovered claude.ai integrations as a list. */
384
384
  export function getClaudeIntegrations() {
385
385
  return Object.values(loadClaudeIntegrations());
386
386
  }
@@ -414,8 +414,9 @@ function saveToolInventory(inv) {
414
414
  * returns every tool Claude Code is actually surfacing — claude_ai_*
415
415
  * connectors, plugins, built-ins, custom MCP servers. Cached for 24h.
416
416
  * This removes any need to hardcode user-specific tool names in the
417
- * system prompt; whatever Claude Desktop is currently connecting to the
418
- * user's account becomes automatically available to the agent.
417
+ * system prompt; whatever the SDK currently surfaces (claude.ai
418
+ * connectors, Composio toolkits, local stdio MCP servers) becomes
419
+ * automatically available to the agent.
419
420
  */
420
421
  export async function probeAvailableTools(force = false) {
421
422
  const cached = loadToolInventory();
@@ -487,9 +488,9 @@ export async function probeAvailableTools(force = false) {
487
488
  * Register every integration found in a tool inventory. The SDK's system
488
489
  * init message (subtype='init') includes a `tools: string[]` with the full
489
490
  * set of tools the agent actually has access to this session — including
490
- * every mcp__claude_ai_* tool Claude Desktop is surfacing. Walking that
491
- * list on init gives us the authoritative, up-to-date integration set
492
- * without waiting for the agent to blindly try each one.
491
+ * every mcp__claude_ai_* tool the SDK is surfacing from claude.ai
492
+ * connectors. Walking that list on init gives us the authoritative,
493
+ * up-to-date integration set without waiting for the agent to blindly try each one.
493
494
  *
494
495
  * Idempotent: if an entry already exists, we merge new tool names into it
495
496
  * and bump `connected = true` without touching firstSeen/lastUsed.
@@ -583,7 +584,7 @@ export function bootstrapClaudeIntegrationsFromAuditLog(auditLogPath) {
583
584
  }
584
585
  if (changed) {
585
586
  saveClaudeIntegrations(integrations);
586
- logger.info({ count: Object.keys(integrations).length }, 'Bootstrapped Claude Desktop integrations from audit log');
587
+ logger.info({ count: Object.keys(integrations).length }, 'Bootstrapped claude.ai integrations from audit log');
587
588
  }
588
589
  }
589
590
  catch (err) {
@@ -14,4 +14,14 @@ export interface BuildChatContextOptions {
14
14
  * SDK then runs with just the bare `claude_code` preset.
15
15
  */
16
16
  export declare function buildChatSystemAppend(opts?: BuildChatContextOptions): string;
17
+ /**
18
+ * Read the long-term memory block for an autonomous run (cron, team-task).
19
+ * Returns the agent-specific MEMORY.md when a hired agent is active, the
20
+ * global MEMORY.md when running as Clementine, or empty when neither
21
+ * exists. Returns a heading-prefixed block ready to drop into a prompt.
22
+ *
23
+ * Heartbeats deliberately skip this — they're tool-free Haiku decisions
24
+ * and a 6KB memory block defeats the cost economy.
25
+ */
26
+ export declare function buildAutonomousMemoryContext(profile?: AgentProfile | null): string;
17
27
  //# sourceMappingURL=run-agent-context.d.ts.map
@@ -41,40 +41,70 @@ function trimTo(text, max) {
41
41
  */
42
42
  export function buildChatSystemAppend(opts = {}) {
43
43
  const blocks = [];
44
- // 1. Soul (personality + voice)
45
- const soul = readFileSafe(SOUL_FILE);
46
- if (soul.trim()) {
47
- blocks.push(`## Identity & Voice\n${trimTo(soul, SOUL_MAX_CHARS)}`);
44
+ const isHiredAgent = !!opts.profile && opts.profile.slug !== 'clementine';
45
+ // 1. Identity & Voice.
46
+ // - Clementine main agent: SOUL.md (the global personality file).
47
+ // - Hired agent main: their own profile.systemPromptBody IS their
48
+ // identity — don't load Clementine's SOUL on top of it.
49
+ if (!isHiredAgent) {
50
+ const soul = readFileSafe(SOUL_FILE);
51
+ if (soul.trim()) {
52
+ blocks.push(`## Identity & Voice\n${trimTo(soul, SOUL_MAX_CHARS)}`);
53
+ }
48
54
  }
49
- // 2. Long-term memory — agent-specific file overrides the global one.
50
- const profileMemoryPath = opts.profile?.slug
55
+ // 2. Long-term memory — agent-specific file when a hired agent is
56
+ // active, otherwise the global one.
57
+ const profileMemoryPath = isHiredAgent
51
58
  ? path.join(AGENTS_DIR, opts.profile.slug, 'MEMORY.md')
52
59
  : null;
53
- let memory = '';
54
60
  if (profileMemoryPath && fs.existsSync(profileMemoryPath)) {
55
- memory = readFileSafe(profileMemoryPath);
61
+ const memory = readFileSafe(profileMemoryPath);
56
62
  if (memory.trim()) {
57
- blocks.push(`## Long-Term Memory (${opts.profile?.name ?? opts.profile?.slug})\n${trimTo(memory, PROFILE_MEMORY_MAX_CHARS)}`);
63
+ blocks.push(`## Long-Term Memory (${opts.profile.name ?? opts.profile.slug})\n${trimTo(memory, PROFILE_MEMORY_MAX_CHARS)}`);
58
64
  }
59
65
  }
60
66
  else {
61
- memory = readFileSafe(MEMORY_FILE);
67
+ const memory = readFileSafe(MEMORY_FILE);
62
68
  if (memory.trim()) {
63
69
  blocks.push(`## Long-Term Memory\n${trimTo(memory, MEMORY_MAX_CHARS)}`);
64
70
  }
65
71
  }
66
72
  // 3. Team roster (only when not running AS a hired agent — Sasha
67
73
  // doesn't need to be told who Sasha is).
68
- if (!opts.profile) {
74
+ if (!isHiredAgent) {
69
75
  const agentsRoster = readFileSafe(AGENTS_FILE);
70
76
  if (agentsRoster.trim()) {
71
77
  blocks.push(`## Team Roster\n${trimTo(agentsRoster, AGENTS_MAX_CHARS)}`);
72
78
  }
73
79
  }
74
- // 4. Profile system prompt body (e.g. Sasha's role description).
80
+ // 4. Profile system prompt body (the hired agent's identity + role).
75
81
  if (opts.profileAppend?.trim()) {
76
82
  blocks.push(opts.profileAppend);
77
83
  }
78
84
  return blocks.join('\n\n');
79
85
  }
86
+ /**
87
+ * Read the long-term memory block for an autonomous run (cron, team-task).
88
+ * Returns the agent-specific MEMORY.md when a hired agent is active, the
89
+ * global MEMORY.md when running as Clementine, or empty when neither
90
+ * exists. Returns a heading-prefixed block ready to drop into a prompt.
91
+ *
92
+ * Heartbeats deliberately skip this — they're tool-free Haiku decisions
93
+ * and a 6KB memory block defeats the cost economy.
94
+ */
95
+ export function buildAutonomousMemoryContext(profile) {
96
+ const isHiredAgent = !!profile && profile.slug !== 'clementine';
97
+ const memoryPath = isHiredAgent
98
+ ? path.join(AGENTS_DIR, profile.slug, 'MEMORY.md')
99
+ : MEMORY_FILE;
100
+ if (!fs.existsSync(memoryPath))
101
+ return '';
102
+ const memory = readFileSafe(memoryPath);
103
+ if (!memory.trim())
104
+ return '';
105
+ const label = isHiredAgent
106
+ ? `Long-Term Memory (${profile.name ?? profile.slug})`
107
+ : 'Long-Term Memory';
108
+ return `## ${label}\n${trimTo(memory, isHiredAgent ? PROFILE_MEMORY_MAX_CHARS : MEMORY_MAX_CHARS)}\n\n`;
109
+ }
80
110
  //# sourceMappingURL=run-agent-context.js.map
@@ -20,6 +20,7 @@ import pino from 'pino';
20
20
  import { BASE_DIR, VAULT_DIR, CRON_PROGRESS_DIR, } from '../config.js';
21
21
  import { runAgent } from './run-agent.js';
22
22
  import { buildExtraMcpForRunAgent } from './run-agent-mcp.js';
23
+ import { buildAutonomousMemoryContext } from './run-agent-context.js';
23
24
  import { listAllGoals } from '../tools/shared.js';
24
25
  const CRON_PROGRESS_PENDING_MAX_ITEMS = 20;
25
26
  const CRON_PROGRESS_NOTES_MAX_CHARS = 2000;
@@ -234,6 +235,10 @@ export async function runAgentCron(opts) {
234
235
  const agentSlug = opts.profile?.slug;
235
236
  const ownerName = process.env.OWNER_NAME ?? 'the user';
236
237
  // ── Compose context blocks (mirrors legacy runCronJob) ─────────────
238
+ // Memory block goes first so the agent reads its long-term context
239
+ // before the run-specific progress/goals/etc. For a hired agent
240
+ // (Ross/Sasha) this is their own MEMORY.md, not Clementine's global.
241
+ const memoryContext = buildAutonomousMemoryContext(opts.profile);
237
242
  const progressContext = buildProgressContext(opts.jobName);
238
243
  const goalContext = buildGoalContext(opts.jobName);
239
244
  const delegationContext = buildDelegationContext(agentSlug);
@@ -246,6 +251,7 @@ export async function runAgentCron(opts) {
246
251
  // spawns isolated sub-agents without a hand-rolled prompt directive.
247
252
  // Final prompt
248
253
  const builtPrompt = `[Scheduled task: ${opts.jobName}]\n\n` +
254
+ memoryContext +
249
255
  progressContext +
250
256
  goalContext +
251
257
  skillContext +
@@ -17,15 +17,21 @@
17
17
  import pino from 'pino';
18
18
  import { runAgent } from './run-agent.js';
19
19
  import { buildExtraMcpForRunAgent } from './run-agent-mcp.js';
20
+ import { buildAutonomousMemoryContext } from './run-agent-context.js';
20
21
  const logger = pino({ name: 'clementine.run-agent-team-task' });
21
22
  export async function runAgentTeamTask(opts) {
22
23
  const taskName = `team-msg:${opts.fromSlug}-to-${opts.profile.slug}`;
23
24
  const now = new Date();
24
25
  const timestamp = now.toISOString().slice(0, 16).replace('T', ' ');
26
+ // Inject the recipient's own long-term memory so they have context
27
+ // about prior work, preferences, and team relationships before
28
+ // processing the message.
29
+ const memoryContext = buildAutonomousMemoryContext(opts.profile);
25
30
  // Match the legacy phase-1 prompt shape so existing agent training
26
31
  // (Sasha/Ross/Nora) keeps responding the same way. Phases 2+ are no
27
32
  // longer needed — the SDK keeps the conversation in one session.
28
33
  const builtPrompt = `[TEAM MESSAGE from ${opts.fromName} (${opts.fromSlug}) — ${timestamp}]\n\n` +
34
+ memoryContext +
29
35
  `You received a direct message from a teammate. Process it fully and autonomously.\n\n` +
30
36
  `MESSAGE:\n${opts.content}\n\n` +
31
37
  `IMPORTANT:\n` +
package/dist/index.js CHANGED
@@ -571,14 +571,22 @@ async function asyncMain() {
571
571
  // refresh_tool_inventory after intentionally adding connectors.
572
572
  const inv = loadToolInventory();
573
573
  if (inv) {
574
- const integrations = new Set();
574
+ const claudeAiIntegrations = new Set();
575
+ let claudeAiToolCount = 0;
575
576
  for (const t of inv.tools) {
576
577
  const m = t.match(/^mcp__claude_ai_([^_]+(?:_[^_]+)*)__/);
577
- if (m)
578
- integrations.add(m[1].replace(/_/g, ' '));
578
+ if (m) {
579
+ claudeAiIntegrations.add(m[1].replace(/_/g, ' '));
580
+ claudeAiToolCount++;
581
+ }
579
582
  }
580
- if (integrations.size > 0) {
581
- logger.info({ integrations: [...integrations].sort(), toolCount: inv.tools.length, probedAt: inv.probedAt }, '🦞 Cached Claude Desktop integrations loaded');
583
+ if (claudeAiIntegrations.size > 0) {
584
+ logger.info({
585
+ claudeAiIntegrations: [...claudeAiIntegrations].sort(),
586
+ claudeAiToolCount,
587
+ totalToolInventory: inv.tools.length,
588
+ probedAt: inv.probedAt,
589
+ }, '🦞 Cached claude.ai integrations loaded');
582
590
  }
583
591
  // After inventory is live, fetch canonical schemas from every stdio
584
592
  // MCP server we can reach, then synthesize auto-skills for every
@@ -325,7 +325,7 @@ export function registerAdminTools(server) {
325
325
  const unique = [...new Set(tools)].sort();
326
326
  writeFileSync(ALLOWED_TOOLS_EXTRA, JSON.stringify(unique, null, 2));
327
327
  }
328
- 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.', {
328
+ 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.ai connector or local MCP server. Owner-DM only.', {
329
329
  name: z.string().describe('Exact tool name (e.g. "mcp__claude_ai_Google_Drive__search_files")'),
330
330
  reason: z.string().optional().describe('Brief note: why you need this tool. For audit trail.'),
331
331
  }, async ({ name, reason }) => {
@@ -368,7 +368,7 @@ export function registerAdminTools(server) {
368
368
  logger.info({ name: trimmed, reason, totalExtras: current.length }, 'allow_tool');
369
369
  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}` : ''}`);
370
370
  });
371
- 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 () => {
371
+ server.tool('refresh_tool_inventory', 'Force a fresh probe of the SDK\'s tool inventory, picking up any claude.ai connectors, Composio toolkits, or local MCP servers 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 () => {
372
372
  const gate = requireOwnerDm();
373
373
  if (!gate.ok)
374
374
  return textResult(gate.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.51",
3
+ "version": "1.18.53",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",