clementine-agent 1.0.0

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.
Files changed (190) hide show
  1. package/.env.example +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +795 -0
  4. package/dist/agent/agent-manager.d.ts +69 -0
  5. package/dist/agent/agent-manager.js +441 -0
  6. package/dist/agent/assistant.d.ts +225 -0
  7. package/dist/agent/assistant.js +3888 -0
  8. package/dist/agent/auto-update.d.ts +32 -0
  9. package/dist/agent/auto-update.js +186 -0
  10. package/dist/agent/daily-planner.d.ts +24 -0
  11. package/dist/agent/daily-planner.js +379 -0
  12. package/dist/agent/execution-advisor.d.ts +10 -0
  13. package/dist/agent/execution-advisor.js +272 -0
  14. package/dist/agent/hooks.d.ts +45 -0
  15. package/dist/agent/hooks.js +564 -0
  16. package/dist/agent/insight-engine.d.ts +66 -0
  17. package/dist/agent/insight-engine.js +225 -0
  18. package/dist/agent/intent-classifier.d.ts +48 -0
  19. package/dist/agent/intent-classifier.js +214 -0
  20. package/dist/agent/link-extractor.d.ts +19 -0
  21. package/dist/agent/link-extractor.js +90 -0
  22. package/dist/agent/mcp-bridge.d.ts +62 -0
  23. package/dist/agent/mcp-bridge.js +435 -0
  24. package/dist/agent/metacognition.d.ts +66 -0
  25. package/dist/agent/metacognition.js +221 -0
  26. package/dist/agent/orchestrator.d.ts +81 -0
  27. package/dist/agent/orchestrator.js +790 -0
  28. package/dist/agent/profiles.d.ts +22 -0
  29. package/dist/agent/profiles.js +91 -0
  30. package/dist/agent/prompt-cache.d.ts +24 -0
  31. package/dist/agent/prompt-cache.js +68 -0
  32. package/dist/agent/prompt-evolver.d.ts +28 -0
  33. package/dist/agent/prompt-evolver.js +279 -0
  34. package/dist/agent/role-scaffolds.d.ts +28 -0
  35. package/dist/agent/role-scaffolds.js +433 -0
  36. package/dist/agent/safe-restart.d.ts +41 -0
  37. package/dist/agent/safe-restart.js +150 -0
  38. package/dist/agent/self-improve.d.ts +66 -0
  39. package/dist/agent/self-improve.js +1706 -0
  40. package/dist/agent/session-event-log.d.ts +114 -0
  41. package/dist/agent/session-event-log.js +233 -0
  42. package/dist/agent/skill-extractor.d.ts +72 -0
  43. package/dist/agent/skill-extractor.js +435 -0
  44. package/dist/agent/source-mods.d.ts +61 -0
  45. package/dist/agent/source-mods.js +230 -0
  46. package/dist/agent/source-preflight.d.ts +25 -0
  47. package/dist/agent/source-preflight.js +100 -0
  48. package/dist/agent/stall-guard.d.ts +62 -0
  49. package/dist/agent/stall-guard.js +109 -0
  50. package/dist/agent/strategic-planner.d.ts +60 -0
  51. package/dist/agent/strategic-planner.js +352 -0
  52. package/dist/agent/team-bus.d.ts +89 -0
  53. package/dist/agent/team-bus.js +556 -0
  54. package/dist/agent/team-router.d.ts +26 -0
  55. package/dist/agent/team-router.js +37 -0
  56. package/dist/agent/tool-loop-detector.d.ts +59 -0
  57. package/dist/agent/tool-loop-detector.js +242 -0
  58. package/dist/agent/workflow-runner.d.ts +36 -0
  59. package/dist/agent/workflow-runner.js +317 -0
  60. package/dist/agent/workflow-variables.d.ts +16 -0
  61. package/dist/agent/workflow-variables.js +62 -0
  62. package/dist/channels/discord-agent-bot.d.ts +101 -0
  63. package/dist/channels/discord-agent-bot.js +881 -0
  64. package/dist/channels/discord-bot-manager.d.ts +80 -0
  65. package/dist/channels/discord-bot-manager.js +262 -0
  66. package/dist/channels/discord-utils.d.ts +51 -0
  67. package/dist/channels/discord-utils.js +293 -0
  68. package/dist/channels/discord.d.ts +12 -0
  69. package/dist/channels/discord.js +1832 -0
  70. package/dist/channels/slack-agent-bot.d.ts +73 -0
  71. package/dist/channels/slack-agent-bot.js +320 -0
  72. package/dist/channels/slack-bot-manager.d.ts +66 -0
  73. package/dist/channels/slack-bot-manager.js +236 -0
  74. package/dist/channels/slack-utils.d.ts +39 -0
  75. package/dist/channels/slack-utils.js +189 -0
  76. package/dist/channels/slack.d.ts +11 -0
  77. package/dist/channels/slack.js +196 -0
  78. package/dist/channels/telegram.d.ts +10 -0
  79. package/dist/channels/telegram.js +235 -0
  80. package/dist/channels/webhook.d.ts +9 -0
  81. package/dist/channels/webhook.js +78 -0
  82. package/dist/channels/whatsapp.d.ts +11 -0
  83. package/dist/channels/whatsapp.js +181 -0
  84. package/dist/cli/chat.d.ts +14 -0
  85. package/dist/cli/chat.js +220 -0
  86. package/dist/cli/cron.d.ts +17 -0
  87. package/dist/cli/cron.js +552 -0
  88. package/dist/cli/dashboard.d.ts +15 -0
  89. package/dist/cli/dashboard.js +17677 -0
  90. package/dist/cli/index.d.ts +3 -0
  91. package/dist/cli/index.js +2474 -0
  92. package/dist/cli/routes/delegations.d.ts +19 -0
  93. package/dist/cli/routes/delegations.js +154 -0
  94. package/dist/cli/routes/digest.d.ts +17 -0
  95. package/dist/cli/routes/digest.js +375 -0
  96. package/dist/cli/routes/goals.d.ts +14 -0
  97. package/dist/cli/routes/goals.js +258 -0
  98. package/dist/cli/routes/workflows.d.ts +18 -0
  99. package/dist/cli/routes/workflows.js +97 -0
  100. package/dist/cli/setup.d.ts +8 -0
  101. package/dist/cli/setup.js +619 -0
  102. package/dist/cli/tunnel.d.ts +35 -0
  103. package/dist/cli/tunnel.js +141 -0
  104. package/dist/config.d.ts +145 -0
  105. package/dist/config.js +278 -0
  106. package/dist/events/bus.d.ts +43 -0
  107. package/dist/events/bus.js +136 -0
  108. package/dist/gateway/cron-scheduler.d.ts +166 -0
  109. package/dist/gateway/cron-scheduler.js +1767 -0
  110. package/dist/gateway/delivery-queue.d.ts +30 -0
  111. package/dist/gateway/delivery-queue.js +110 -0
  112. package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
  113. package/dist/gateway/heartbeat-scheduler.js +1298 -0
  114. package/dist/gateway/heartbeat.d.ts +3 -0
  115. package/dist/gateway/heartbeat.js +3 -0
  116. package/dist/gateway/lanes.d.ts +24 -0
  117. package/dist/gateway/lanes.js +76 -0
  118. package/dist/gateway/notifications.d.ts +29 -0
  119. package/dist/gateway/notifications.js +75 -0
  120. package/dist/gateway/router.d.ts +210 -0
  121. package/dist/gateway/router.js +1330 -0
  122. package/dist/index.d.ts +12 -0
  123. package/dist/index.js +1015 -0
  124. package/dist/memory/chunker.d.ts +28 -0
  125. package/dist/memory/chunker.js +226 -0
  126. package/dist/memory/consolidation.d.ts +44 -0
  127. package/dist/memory/consolidation.js +171 -0
  128. package/dist/memory/context-assembler.d.ts +50 -0
  129. package/dist/memory/context-assembler.js +149 -0
  130. package/dist/memory/embeddings.d.ts +38 -0
  131. package/dist/memory/embeddings.js +180 -0
  132. package/dist/memory/graph-store.d.ts +66 -0
  133. package/dist/memory/graph-store.js +613 -0
  134. package/dist/memory/mmr.d.ts +21 -0
  135. package/dist/memory/mmr.js +75 -0
  136. package/dist/memory/search.d.ts +26 -0
  137. package/dist/memory/search.js +67 -0
  138. package/dist/memory/store.d.ts +530 -0
  139. package/dist/memory/store.js +2022 -0
  140. package/dist/security/integrity.d.ts +24 -0
  141. package/dist/security/integrity.js +58 -0
  142. package/dist/security/patterns.d.ts +34 -0
  143. package/dist/security/patterns.js +110 -0
  144. package/dist/security/scanner.d.ts +32 -0
  145. package/dist/security/scanner.js +263 -0
  146. package/dist/tools/admin-tools.d.ts +12 -0
  147. package/dist/tools/admin-tools.js +1278 -0
  148. package/dist/tools/external-tools.d.ts +11 -0
  149. package/dist/tools/external-tools.js +1327 -0
  150. package/dist/tools/goal-tools.d.ts +9 -0
  151. package/dist/tools/goal-tools.js +159 -0
  152. package/dist/tools/mcp-server.d.ts +13 -0
  153. package/dist/tools/mcp-server.js +141 -0
  154. package/dist/tools/memory-tools.d.ts +10 -0
  155. package/dist/tools/memory-tools.js +568 -0
  156. package/dist/tools/session-tools.d.ts +6 -0
  157. package/dist/tools/session-tools.js +146 -0
  158. package/dist/tools/shared.d.ts +216 -0
  159. package/dist/tools/shared.js +340 -0
  160. package/dist/tools/team-tools.d.ts +6 -0
  161. package/dist/tools/team-tools.js +447 -0
  162. package/dist/tools/tool-meta.d.ts +34 -0
  163. package/dist/tools/tool-meta.js +133 -0
  164. package/dist/tools/vault-tools.d.ts +8 -0
  165. package/dist/tools/vault-tools.js +457 -0
  166. package/dist/types.d.ts +716 -0
  167. package/dist/types.js +16 -0
  168. package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
  169. package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
  170. package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
  171. package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
  172. package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
  173. package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
  174. package/dist/vault-migrations/helpers.d.ts +14 -0
  175. package/dist/vault-migrations/helpers.js +44 -0
  176. package/dist/vault-migrations/runner.d.ts +14 -0
  177. package/dist/vault-migrations/runner.js +139 -0
  178. package/dist/vault-migrations/types.d.ts +42 -0
  179. package/dist/vault-migrations/types.js +9 -0
  180. package/install.sh +320 -0
  181. package/package.json +84 -0
  182. package/scripts/postinstall.js +125 -0
  183. package/vault/00-System/AGENTS.md +66 -0
  184. package/vault/00-System/CRON.md +71 -0
  185. package/vault/00-System/HEARTBEAT.md +58 -0
  186. package/vault/00-System/MEMORY.md +16 -0
  187. package/vault/00-System/SOUL.md +96 -0
  188. package/vault/05-Tasks/TASKS.md +19 -0
  189. package/vault/06-Templates/_Daily-Template.md +28 -0
  190. package/vault/06-Templates/_People-Template.md +22 -0
@@ -0,0 +1,447 @@
1
+ /**
2
+ * Clementine TypeScript — Team, Agent CRUD, and Delegation MCP tools.
3
+ */
4
+ import { randomBytes } from 'node:crypto';
5
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { z } from 'zod';
8
+ import { ACTIVE_AGENT_SLUG, AGENTS_DIR, BASE_DIR, DELEGATIONS_BASE, PROFILES_DIR, TEAM_COMMS_LOG, env, logger, parseTasks, textResult, } from './shared.js';
9
+ import { todayISO } from '../gateway/cron-scheduler.js';
10
+ async function loadTeamAgents() {
11
+ const matterMod = await import('gray-matter');
12
+ const agents = [];
13
+ const seen = new Set();
14
+ if (existsSync(AGENTS_DIR)) {
15
+ try {
16
+ for (const slug of readdirSync(AGENTS_DIR, { withFileTypes: true }).filter(d => d.isDirectory() && !d.name.startsWith('_')).map(d => d.name)) {
17
+ const agentFile = path.join(AGENTS_DIR, slug, 'agent.md');
18
+ if (!existsSync(agentFile))
19
+ continue;
20
+ try {
21
+ const { data } = matterMod.default(readFileSync(agentFile, 'utf-8'));
22
+ const channelName = data.channelName ? String(data.channelName) : '';
23
+ if (!channelName)
24
+ continue;
25
+ seen.add(slug);
26
+ agents.push({ slug, name: String(data.name ?? slug), channelName, canMessage: Array.isArray(data.canMessage) ? data.canMessage.map(String) : [], description: String(data.description ?? '') });
27
+ }
28
+ catch { /* skip */ }
29
+ }
30
+ }
31
+ catch { /* agents dir not readable */ }
32
+ }
33
+ if (existsSync(PROFILES_DIR)) {
34
+ for (const file of readdirSync(PROFILES_DIR).filter(f => f.endsWith('.md') && !f.startsWith('_'))) {
35
+ try {
36
+ const slug = file.replace(/\.md$/, '');
37
+ if (seen.has(slug))
38
+ continue;
39
+ const { data } = (await import('gray-matter')).default(readFileSync(path.join(PROFILES_DIR, file), 'utf-8'));
40
+ const channelName = data.channelName ? String(data.channelName) : '';
41
+ if (!channelName)
42
+ continue;
43
+ agents.push({ slug, name: String(data.name ?? slug), channelName, canMessage: Array.isArray(data.canMessage) ? data.canMessage.map(String) : [], description: String(data.description ?? '') });
44
+ }
45
+ catch { /* skip */ }
46
+ }
47
+ }
48
+ return agents;
49
+ }
50
+ function assertAgentCrudAllowed(action) {
51
+ if (ACTIVE_AGENT_SLUG) {
52
+ throw new Error(`Only the primary agent or owner can ${action}. Current agent '${ACTIVE_AGENT_SLUG}' is not authorized.`);
53
+ }
54
+ }
55
+ const teamMessageDelivered = new Map();
56
+ // ── Registration ────────────────────────────────────────────────────────
57
+ export function registerTeamTools(server) {
58
+ server.tool('team_list', 'List all team agents — their names, channel bindings, and messaging permissions.', { _empty: z.string().optional().describe('(no parameters needed)') }, async () => {
59
+ const agents = await loadTeamAgents();
60
+ if (agents.length === 0)
61
+ return textResult('No team agents configured.');
62
+ const callerSlug = process.env.CLEMENTINE_TEAM_AGENT ?? '';
63
+ const isPrimary = !agents.find(a => a.slug === callerSlug);
64
+ const lines = agents.map(a => `- ${a.name} (${a.slug}): #${a.channelName}, canMessage=[${a.canMessage.join(', ')}]`);
65
+ const header = isPrimary ? 'Team Agents (you are the primary agent — you can message any agent below):' : 'Team Agents:';
66
+ return textResult(`${header}\n${lines.join('\n')}`);
67
+ });
68
+ server.tool('team_message', 'Send a message to another team agent. You may only send ONE message per recipient per conversation.', {
69
+ to_agent: z.string().describe('Slug of the target agent'),
70
+ message: z.string().describe('Message content to send'),
71
+ depth: z.number().optional().describe('Message depth counter (auto-incremented). Do not set manually.'),
72
+ }, async ({ to_agent, message, depth }) => {
73
+ const priorDelivery = teamMessageDelivered.get(to_agent);
74
+ if (priorDelivery) {
75
+ return textResult(`ALREADY DELIVERED: Your message to ${to_agent} was successfully delivered at ${new Date(priorDelivery.at).toLocaleTimeString()}. Do NOT resend.`);
76
+ }
77
+ const agents = await loadTeamAgents();
78
+ const callerSlug = process.env.CLEMENTINE_TEAM_AGENT ?? '';
79
+ if (!callerSlug)
80
+ return textResult('Error: Cannot determine which agent is calling team_message.');
81
+ const caller = agents.find(a => a.slug === callerSlug);
82
+ if (caller && !caller.canMessage.includes(to_agent)) {
83
+ return textResult(`Error: Agent '${callerSlug}' is not authorized to message '${to_agent}'. Allowed: ${caller.canMessage.join(', ') || 'none'}`);
84
+ }
85
+ const target = agents.find(a => a.slug === to_agent);
86
+ if (!target)
87
+ return textResult(`Error: Target agent '${to_agent}' not found.`);
88
+ const msgDepth = depth ?? 0;
89
+ if (msgDepth >= 3)
90
+ return textResult('Error: Message depth limit reached (3).');
91
+ // Try synchronous delivery via daemon HTTP API
92
+ const dashboardPort = env['DASHBOARD_PORT'] ?? '3030';
93
+ let dashboardToken = '';
94
+ try {
95
+ dashboardToken = readFileSync(path.join(BASE_DIR, '.dashboard-token'), 'utf-8').trim();
96
+ }
97
+ catch { /* */ }
98
+ try {
99
+ const res = await fetch(`http://127.0.0.1:${dashboardPort}/api/team/message`, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json', ...(dashboardToken ? { 'Authorization': `Bearer ${dashboardToken}` } : {}) },
102
+ body: JSON.stringify({ from_agent: callerSlug, to_agent, message, depth: msgDepth }),
103
+ signal: AbortSignal.timeout(120_000),
104
+ });
105
+ const data = await res.json();
106
+ if (data.ok && data.delivered) {
107
+ teamMessageDelivered.set(to_agent, { at: Date.now(), content: message });
108
+ return data.response ? textResult(`${target.name} responded:\n\n${data.response}`) : textResult(`Message delivered to ${target.name}. No response captured.`);
109
+ }
110
+ if (data.ok && !data.delivered) {
111
+ teamMessageDelivered.set(to_agent, { at: Date.now(), content: message });
112
+ return textResult(`Message queued for ${target.name} — they'll see it on their next interaction.`);
113
+ }
114
+ if (data.error)
115
+ return textResult(`Error: ${data.error}`);
116
+ }
117
+ catch { /* daemon unreachable — JSONL fallback */ }
118
+ const msgId = randomBytes(4).toString('hex');
119
+ const record = { id: msgId, fromAgent: callerSlug, toAgent: to_agent, content: message, timestamp: new Date().toISOString(), delivered: false, depth: msgDepth };
120
+ const logDir = path.dirname(TEAM_COMMS_LOG);
121
+ if (!existsSync(logDir))
122
+ mkdirSync(logDir, { recursive: true });
123
+ appendFileSync(TEAM_COMMS_LOG, JSON.stringify(record) + '\n');
124
+ teamMessageDelivered.set(to_agent, { at: Date.now(), content: message });
125
+ return textResult(`Message queued for ${target.name} (${to_agent}). ID: ${msgId}.`);
126
+ });
127
+ server.tool('team_request', 'Send a structured request to another team agent and wait for their response (blocks up to 5 min).', {
128
+ to_agent: z.string().describe('Slug of the target agent'),
129
+ request: z.string().describe('The question or request content'),
130
+ timeout_seconds: z.number().optional().describe('Timeout in seconds (default: 300, max: 600)'),
131
+ }, async ({ to_agent, request, timeout_seconds }) => {
132
+ const callerSlug = process.env.CLEMENTINE_TEAM_AGENT ?? '';
133
+ if (!callerSlug)
134
+ return textResult('Error: Cannot determine calling agent.');
135
+ const timeoutMs = Math.min((timeout_seconds ?? 300) * 1000, 600_000);
136
+ const dashboardPort = env['DASHBOARD_PORT'] ?? '3030';
137
+ let dashboardToken = '';
138
+ try {
139
+ dashboardToken = readFileSync(path.join(BASE_DIR, '.dashboard-token'), 'utf-8').trim();
140
+ }
141
+ catch { /* */ }
142
+ try {
143
+ const res = await fetch(`http://127.0.0.1:${dashboardPort}/api/team/request`, {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json', ...(dashboardToken ? { 'Authorization': `Bearer ${dashboardToken}` } : {}) },
146
+ body: JSON.stringify({ from_agent: callerSlug, to_agent, content: request, timeout_ms: timeoutMs }),
147
+ signal: AbortSignal.timeout(timeoutMs + 10_000),
148
+ });
149
+ const data = await res.json();
150
+ if (data.ok && data.response)
151
+ return textResult(`Response from ${to_agent}:\n\n${data.response}`);
152
+ if (data.timed_out)
153
+ return textResult(`Request to ${to_agent} timed out after ${timeout_seconds ?? 300}s.`);
154
+ return textResult(`Error: ${data.error ?? 'Unknown error'}`);
155
+ }
156
+ catch (err) {
157
+ return textResult(`Error sending request: ${String(err)}`);
158
+ }
159
+ });
160
+ server.tool('team_pending_requests', 'Check for pending requests from other team agents that need your response.', { _empty: z.string().optional().describe('(no parameters needed)') }, async () => {
161
+ const callerSlug = process.env.CLEMENTINE_TEAM_AGENT ?? '';
162
+ if (!callerSlug)
163
+ return textResult('Error: Cannot determine calling agent.');
164
+ const dashboardPort = env['DASHBOARD_PORT'] ?? '3030';
165
+ let dashboardToken = '';
166
+ try {
167
+ dashboardToken = readFileSync(path.join(BASE_DIR, '.dashboard-token'), 'utf-8').trim();
168
+ }
169
+ catch { /* */ }
170
+ try {
171
+ const res = await fetch(`http://127.0.0.1:${dashboardPort}/api/team/pending-requests?agent=${callerSlug}`, {
172
+ headers: dashboardToken ? { 'Authorization': `Bearer ${dashboardToken}` } : {},
173
+ signal: AbortSignal.timeout(10_000),
174
+ });
175
+ const data = await res.json();
176
+ if (!data.ok)
177
+ return textResult(`Error: ${data.error ?? 'Failed to fetch pending requests'}`);
178
+ const requests = data.requests ?? [];
179
+ if (requests.length === 0)
180
+ return textResult('No pending requests.');
181
+ const lines = requests.map((r) => `- **[REPLY NEEDED]** From ${r.fromAgent} (${r.requestId}): ${r.content.slice(0, 200)}${r.expectedBy ? ` — expected by ${r.expectedBy}` : ''}`);
182
+ return textResult(`## Pending Requests (${requests.length})\n${lines.join('\n')}\n\nUse team_message to respond.`);
183
+ }
184
+ catch {
185
+ if (existsSync(TEAM_COMMS_LOG)) {
186
+ try {
187
+ const logLines = readFileSync(TEAM_COMMS_LOG, 'utf-8').trim().split('\n').filter(Boolean);
188
+ const pendingReqs = logLines.slice(-100).map(l => { try {
189
+ return JSON.parse(l);
190
+ }
191
+ catch {
192
+ return null;
193
+ } })
194
+ .filter((m) => m && m.protocol === 'request' && m.toAgent === callerSlug && !m.response);
195
+ if (pendingReqs.length === 0)
196
+ return textResult('No pending requests found.');
197
+ const formatted = pendingReqs.map((r) => `- **[REPLY NEEDED]** From ${r.fromAgent}: ${r.content.slice(0, 200)}`);
198
+ return textResult(`## Pending Requests (${pendingReqs.length})\n${formatted.join('\n')}`);
199
+ }
200
+ catch {
201
+ return textResult('No pending requests found.');
202
+ }
203
+ }
204
+ return textResult('No pending requests found (daemon unreachable).');
205
+ }
206
+ });
207
+ // ── Agent CRUD ─────────────────────────────────────────────────────
208
+ server.tool('create_agent', 'Create a new scoped agent with its own personality, tools, crons, and project binding.', {
209
+ name: z.string().describe('Display name'), description: z.string().describe('Short description'),
210
+ personality: z.string().optional().describe('Full system prompt body'),
211
+ channel_name: z.string().optional().describe('Discord channel name'),
212
+ project: z.string().optional().describe('Project name binding'),
213
+ tools: z.array(z.string()).optional().describe('Tool whitelist'),
214
+ model: z.string().optional().describe('Model tier: "haiku", "sonnet", "opus"'),
215
+ can_message: z.array(z.string()).optional().describe('Agent slugs this agent can message'),
216
+ tier: z.number().optional().describe('Security tier (1=read-only, 2=read-write). Default: 2'),
217
+ }, async ({ name, description, personality, channel_name, project, tools, model, can_message, tier }) => {
218
+ assertAgentCrudAllowed('create agents');
219
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
220
+ const agentDir = path.join(AGENTS_DIR, slug);
221
+ if (existsSync(path.join(agentDir, 'agent.md')))
222
+ return textResult(`Error: Agent '${slug}' already exists.`);
223
+ mkdirSync(agentDir, { recursive: true });
224
+ const frontmatter = { name, description, tier: Math.min(tier ?? 2, 2) };
225
+ if (model)
226
+ frontmatter.model = model;
227
+ if (channel_name)
228
+ frontmatter.channelName = channel_name;
229
+ if (can_message?.length)
230
+ frontmatter.canMessage = can_message;
231
+ if (tools?.length)
232
+ frontmatter.allowedTools = tools;
233
+ if (project)
234
+ frontmatter.project = project;
235
+ const body = personality || `You are ${name}. ${description}`;
236
+ const matterMod = await import('gray-matter');
237
+ writeFileSync(path.join(agentDir, 'agent.md'), matterMod.default.stringify(body, frontmatter));
238
+ // Scaffold per-agent context files
239
+ const tasksFile = path.join(agentDir, 'TASKS.md');
240
+ if (!existsSync(tasksFile)) {
241
+ writeFileSync(tasksFile, `---\ntype: task-list\ntags:\n - tasks\n---\n\n# Tasks\n\n## Pending\n\n## In Progress\n\n## Completed\n`);
242
+ }
243
+ const wmFile = path.join(agentDir, 'working-memory.md');
244
+ if (!existsSync(wmFile)) {
245
+ writeFileSync(wmFile, `# Working Memory\n\n*Scratchpad for ${name}. Updated during runs and conversations.*\n`);
246
+ }
247
+ const goalsDir = path.join(agentDir, 'goals');
248
+ if (!existsSync(goalsDir))
249
+ mkdirSync(goalsDir, { recursive: true });
250
+ const dailyNotesDir = path.join(agentDir, 'daily-notes');
251
+ if (!existsSync(dailyNotesDir))
252
+ mkdirSync(dailyNotesDir, { recursive: true });
253
+ const cronFile = path.join(agentDir, 'CRON.md');
254
+ if (!existsSync(cronFile)) {
255
+ writeFileSync(cronFile, `---\ntype: cron-config\njobs: []\n---\n\n# Cron Jobs\n\n*No scheduled jobs yet.*\n`);
256
+ }
257
+ return textResult(`Created agent '${name}' (${slug}).${channel_name ? ` Channel: #${channel_name}` : ''}${project ? ` Project: ${project}` : ''}`);
258
+ });
259
+ server.tool('update_agent', 'Update an existing agent\'s configuration. Only specified fields are changed.', {
260
+ slug: z.string().describe('Agent slug'), name: z.string().optional(), description: z.string().optional(),
261
+ personality: z.string().optional(), channel_name: z.string().optional(), project: z.string().optional(),
262
+ tools: z.array(z.string()).optional(), model: z.string().optional(), can_message: z.array(z.string()).optional(),
263
+ tier: z.number().optional(),
264
+ }, async ({ slug, name, description, personality, channel_name, project, tools, model, can_message, tier }) => {
265
+ assertAgentCrudAllowed('update agents');
266
+ const agentFile = path.join(AGENTS_DIR, slug, 'agent.md');
267
+ if (!existsSync(agentFile))
268
+ return textResult(`Error: Agent '${slug}' not found.`);
269
+ const matterMod = await import('gray-matter');
270
+ const { data: meta, content: body } = matterMod.default(readFileSync(agentFile, 'utf-8'));
271
+ if (name !== undefined)
272
+ meta.name = name;
273
+ if (description !== undefined)
274
+ meta.description = description;
275
+ if (tier !== undefined)
276
+ meta.tier = Math.min(tier, 2);
277
+ if (model !== undefined)
278
+ meta.model = model;
279
+ if (channel_name !== undefined)
280
+ meta.channelName = channel_name;
281
+ if (can_message !== undefined)
282
+ meta.canMessage = can_message;
283
+ if (tools !== undefined)
284
+ meta.allowedTools = tools;
285
+ if (project !== undefined)
286
+ meta.project = project;
287
+ writeFileSync(agentFile, matterMod.default.stringify(personality ?? body, meta));
288
+ const changed = [name !== undefined && 'name', description !== undefined && 'description', personality !== undefined && 'personality',
289
+ channel_name !== undefined && 'channelName', project !== undefined && 'project', tools !== undefined && 'tools',
290
+ model !== undefined && 'model', can_message !== undefined && 'canMessage', tier !== undefined && 'tier'].filter(Boolean);
291
+ return textResult(`Updated agent '${slug}'. Changes: ${changed.join(', ')}`);
292
+ });
293
+ server.tool('delete_agent', 'Delete an agent and its entire directory.', { slug: z.string().describe('Agent slug'), confirm: z.boolean().describe('Must be true to confirm') }, async ({ slug, confirm }) => {
294
+ assertAgentCrudAllowed('delete agents');
295
+ if (!confirm)
296
+ return textResult('Deletion cancelled — set confirm=true.');
297
+ const agentDir = path.join(AGENTS_DIR, slug);
298
+ if (!existsSync(agentDir))
299
+ return textResult(`Error: Agent '${slug}' not found.`);
300
+ const { rmSync } = await import('node:fs');
301
+ rmSync(agentDir, { recursive: true, force: true });
302
+ return textResult(`Deleted agent '${slug}'.`);
303
+ });
304
+ // ── Delegation ─────────────────────────────────────────────────────
305
+ server.tool('delegate_task', 'Delegate a task to a team agent. Creates a structured task in their queue.', {
306
+ to_agent: z.string().describe('Slug of the target agent'),
307
+ task: z.string().describe('What needs to be done'),
308
+ expected_output: z.string().describe('What the result should look like'),
309
+ }, async ({ to_agent, task, expected_output }) => {
310
+ const tasksDir = path.join(DELEGATIONS_BASE, to_agent, 'tasks');
311
+ if (!existsSync(tasksDir))
312
+ mkdirSync(tasksDir, { recursive: true });
313
+ const id = randomBytes(4).toString('hex');
314
+ const callerSlug = process.env.CLEMENTINE_TEAM_AGENT || 'clementine';
315
+ const delegation = { id, fromAgent: callerSlug, toAgent: to_agent, task, expectedOutput: expected_output, status: 'pending', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
316
+ writeFileSync(path.join(tasksDir, `${id}.json`), JSON.stringify(delegation, null, 2));
317
+ logger.info({ delegationId: id, from: callerSlug, to: to_agent }, 'Task delegated');
318
+ return textResult(`Task delegated to ${to_agent} (ID: ${id}).`);
319
+ });
320
+ server.tool('check_delegation', 'Check the status of a delegated task or list all delegations for an agent.', { id: z.string().optional().describe('Delegation ID'), agent: z.string().optional().describe('Agent slug to list all') }, async ({ id, agent }) => {
321
+ if (id) {
322
+ if (!existsSync(DELEGATIONS_BASE))
323
+ return textResult('No delegations found.');
324
+ for (const slug of readdirSync(DELEGATIONS_BASE, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)) {
325
+ const taskFile = path.join(DELEGATIONS_BASE, slug, 'tasks', `${id}.json`);
326
+ if (existsSync(taskFile)) {
327
+ const d = JSON.parse(readFileSync(taskFile, 'utf-8'));
328
+ const lines = [`**Delegation ${id}**`, `From: ${d.fromAgent} → To: ${d.toAgent}`, `Status: ${d.status}`, `Task: ${d.task}`, `Expected: ${d.expectedOutput}`, `Created: ${d.createdAt}`];
329
+ if (d.result)
330
+ lines.push(`Result: ${d.result}`);
331
+ return textResult(lines.join('\n'));
332
+ }
333
+ }
334
+ return textResult(`Delegation ${id} not found.`);
335
+ }
336
+ if (agent) {
337
+ const tasksDir = path.join(DELEGATIONS_BASE, agent, 'tasks');
338
+ if (!existsSync(tasksDir))
339
+ return textResult(`No delegations for ${agent}.`);
340
+ const files = readdirSync(tasksDir).filter(f => f.endsWith('.json'));
341
+ if (files.length === 0)
342
+ return textResult(`No delegations for ${agent}.`);
343
+ const delegations = files.map(f => { try {
344
+ return JSON.parse(readFileSync(path.join(tasksDir, f), 'utf-8'));
345
+ }
346
+ catch {
347
+ return null;
348
+ } }).filter(Boolean);
349
+ const lines = delegations.map((d) => `- [${d.status.toUpperCase()}] ${d.id}: "${d.task.slice(0, 80)}" (from ${d.fromAgent})`);
350
+ return textResult(`Delegations for ${agent} (${delegations.length}):\n${lines.join('\n')}`);
351
+ }
352
+ return textResult('Provide "id" or "agent" parameter.');
353
+ });
354
+ // ── Team Status ────────────────────────────────────────────────────
355
+ server.tool('team_status', 'Get a summary of all team agents: their recent daily notes, pending tasks count, and active goals. Use this for morning briefings and cross-agent coordination.', {
356
+ agent: z.string().optional().describe('Specific agent slug to check. If omitted, returns all agents.'),
357
+ include_tasks: z.boolean().optional().describe('Include pending task count (default true)'),
358
+ include_goals: z.boolean().optional().describe('Include active goals (default true)'),
359
+ include_daily_notes: z.boolean().optional().describe('Include last 3 days of daily notes (default true)'),
360
+ }, async ({ agent, include_tasks = true, include_goals = true, include_daily_notes = true }) => {
361
+ const agentsBase = AGENTS_DIR;
362
+ if (!existsSync(agentsBase))
363
+ return textResult('No agents found.');
364
+ const agentSlugs = readdirSync(agentsBase, { withFileTypes: true })
365
+ .filter(d => d.isDirectory())
366
+ .map(d => d.name)
367
+ .filter(n => !agent || n === agent);
368
+ if (!agentSlugs.length)
369
+ return textResult('No agents found.');
370
+ const matterMod = await import('gray-matter');
371
+ const parts = ['# Team Status\n'];
372
+ for (const slug of agentSlugs) {
373
+ const agentDir = path.join(agentsBase, slug);
374
+ const agentMdPath = path.join(agentDir, 'agent.md');
375
+ let agentName = slug;
376
+ try {
377
+ const raw = readFileSync(agentMdPath, 'utf-8');
378
+ const parsed = matterMod.default(raw);
379
+ agentName = parsed.data.name ?? slug;
380
+ }
381
+ catch { }
382
+ parts.push(`## ${agentName} (${slug})`);
383
+ // Tasks
384
+ if (include_tasks) {
385
+ const tasksFile = path.join(agentDir, 'TASKS.md');
386
+ if (existsSync(tasksFile)) {
387
+ const body = readFileSync(tasksFile, 'utf-8');
388
+ const tasks = parseTasks(body);
389
+ const pending = tasks.filter(t => t.status === 'pending');
390
+ const overdue = pending.filter(t => t.due && t.due < todayISO());
391
+ parts.push(`**Tasks:** ${pending.length} pending${overdue.length > 0 ? `, ${overdue.length} overdue` : ''}`);
392
+ if (pending.length > 0) {
393
+ parts.push(pending.slice(0, 3).map(t => ` - ${t.text.slice(0, 100)}`).join('\n'));
394
+ }
395
+ }
396
+ else {
397
+ parts.push('**Tasks:** No task file yet');
398
+ }
399
+ }
400
+ // Goals
401
+ if (include_goals) {
402
+ const goalsDir = path.join(agentDir, 'goals');
403
+ if (existsSync(goalsDir)) {
404
+ const goalFiles = readdirSync(goalsDir).filter(f => f.endsWith('.json'));
405
+ const activeGoals = goalFiles
406
+ .map(f => { try {
407
+ return JSON.parse(readFileSync(path.join(goalsDir, f), 'utf-8'));
408
+ }
409
+ catch {
410
+ return null;
411
+ } })
412
+ .filter((g) => g !== null && g.status === 'active');
413
+ if (activeGoals.length > 0) {
414
+ parts.push(`**Goals (${activeGoals.length} active):**`);
415
+ for (const g of activeGoals.slice(0, 3)) {
416
+ const progress = g.progress ? ` — ${g.progress}` : '';
417
+ parts.push(` - ${g.title}${progress}`);
418
+ }
419
+ }
420
+ }
421
+ }
422
+ // Daily notes
423
+ if (include_daily_notes) {
424
+ const dailyDir = path.join(agentDir, 'daily-notes');
425
+ if (existsSync(dailyDir)) {
426
+ const notes = readdirSync(dailyDir)
427
+ .filter(f => f.endsWith('.md'))
428
+ .sort().reverse().slice(0, 3);
429
+ if (notes.length > 0) {
430
+ parts.push('**Recent Activity:**');
431
+ for (const note of notes) {
432
+ try {
433
+ const content = readFileSync(path.join(dailyDir, note), 'utf-8');
434
+ const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#')).slice(0, 3);
435
+ parts.push(` ${note.replace('.md', '')}: ${lines[0]?.slice(0, 120) ?? '(empty)'}`);
436
+ }
437
+ catch { }
438
+ }
439
+ }
440
+ }
441
+ }
442
+ parts.push('');
443
+ }
444
+ return textResult(parts.join('\n'));
445
+ });
446
+ }
447
+ //# sourceMappingURL=team-tools.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Centralized tool descriptions for MCP tools.
3
+ *
4
+ * Inspired by Anthropic's finding that better tool descriptions yield a 40%
5
+ * decrease in task completion time. Descriptions here are agent-facing:
6
+ * they explain WHEN to use each tool, what to expect, and common pitfalls.
7
+ *
8
+ * Usage: import { getToolDescription } from './tool-meta.js';
9
+ * const desc = getToolDescription('memory_search') ?? defaultDesc;
10
+ */
11
+ export interface ToolMeta {
12
+ /** Agent-facing description: when to use, what it returns, tips. */
13
+ description: string;
14
+ /** One-line example showing a typical invocation context. */
15
+ exampleUsage?: string;
16
+ /** What the return value looks like. */
17
+ returnHint?: string;
18
+ /** Guidance for tools that paginate or truncate. */
19
+ paginationNote?: string;
20
+ }
21
+ /**
22
+ * Get the enhanced description for a tool, or null if not defined.
23
+ * Falls back gracefully so existing inline descriptions still work.
24
+ */
25
+ export declare function getToolDescription(toolName: string): string | null;
26
+ /**
27
+ * Get full tool metadata for a tool.
28
+ */
29
+ export declare function getToolMeta(toolName: string): ToolMeta | null;
30
+ /**
31
+ * Get all tool names that have enhanced descriptions.
32
+ */
33
+ export declare function getEnhancedToolNames(): string[];
34
+ //# sourceMappingURL=tool-meta.d.ts.map
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Centralized tool descriptions for MCP tools.
3
+ *
4
+ * Inspired by Anthropic's finding that better tool descriptions yield a 40%
5
+ * decrease in task completion time. Descriptions here are agent-facing:
6
+ * they explain WHEN to use each tool, what to expect, and common pitfalls.
7
+ *
8
+ * Usage: import { getToolDescription } from './tool-meta.js';
9
+ * const desc = getToolDescription('memory_search') ?? defaultDesc;
10
+ */
11
+ const TOOL_META = {
12
+ // ── Memory & Vault ────────────────────────────────────────────────
13
+ working_memory: {
14
+ description: 'Persistent scratchpad that survives across conversations. Use to jot down current project context, TODOs, reminders, or anything you need to remember for next time. Actions: read, append, replace, clear. ALWAYS read before replacing to avoid overwriting useful notes.',
15
+ exampleUsage: 'Before starting complex work, read working_memory to check for context from prior sessions.',
16
+ returnHint: 'Full working memory contents (markdown text).',
17
+ },
18
+ memory_search: {
19
+ description: 'Full-text search across all vault notes. Best for finding specific keywords or phrases. For broader semantic matching, use memory_recall instead. Results include file path, section heading, and relevance score.',
20
+ exampleUsage: 'Use when the user asks "what did we discuss about X" or you need to find a specific note.',
21
+ returnHint: 'Ranked list: **file > section** (score) — preview text.',
22
+ paginationNote: 'Default limit is 20 results. For broad queries, start with limit=5 and increase only if needed.',
23
+ },
24
+ memory_recall: {
25
+ description: 'Context retrieval combining text relevance + recency. Better than memory_search for finding related content — it considers how recently notes were updated. Use this as your default "what do I know about X" tool.',
26
+ exampleUsage: 'Use before responding to questions about people, projects, or topics the user has discussed before.',
27
+ returnHint: 'Ranked chunks with source file, category, and content preview.',
28
+ },
29
+ memory_read: {
30
+ description: "Read a note from the Obsidian vault. Shortcuts: 'today' (daily note), 'yesterday', 'memory' (MEMORY.md), 'tasks' (TASKS.md), 'heartbeat', 'cron', 'soul'. Or pass a relative path like '03-Projects/my-project.md'.",
31
+ exampleUsage: "memory_read('today') to check what happened today before making plans.",
32
+ returnHint: 'Full note content with YAML frontmatter.',
33
+ },
34
+ memory_write: {
35
+ description: "Write or append to a vault note. Actions: 'append_daily' (add to today's log — use for recording activities), 'update_memory' (update a section of MEMORY.md — use for durable facts), 'write_note' (write/overwrite a note), 'update_identity' (set your identity context).",
36
+ exampleUsage: "After completing a task, use append_daily to record what was done.",
37
+ },
38
+ memory_connections: {
39
+ description: 'Query the wikilink graph — find all notes connected to/from a given note. Use to discover related context you might not find via text search.',
40
+ returnHint: 'List of linked notes with titles and paths.',
41
+ },
42
+ memory_timeline: {
43
+ description: 'Chronological view of memory/vault changes within a date range. Use for "what happened last week" or "show me recent changes" queries.',
44
+ returnHint: 'Date-ordered list of changes with file, section, and timestamp.',
45
+ },
46
+ transcript_search: {
47
+ description: 'Search past conversation transcripts by keyword. Returns matching turns with session context. Use when you need to recall what was said in a specific conversation.',
48
+ returnHint: 'Matching conversation turns with session key and timestamp.',
49
+ },
50
+ // ── External APIs ────────────────────────────────────────────────
51
+ outlook_inbox: {
52
+ description: 'Read recent emails from Outlook inbox. Returns sender, subject, date, preview, and read/unread status. Use count=5 for a quick check, count=25 for comprehensive review.',
53
+ exampleUsage: 'Check inbox at the start of a work session or when user asks about emails.',
54
+ returnHint: 'JSON array of email objects with id, from, subject, date, preview, unread, hasAttachments.',
55
+ paginationNote: 'Max 25 emails per call. Use outlook_search for targeted lookups instead of fetching all.',
56
+ },
57
+ outlook_search: {
58
+ description: 'Search emails by keyword across subject, body, and sender. More efficient than scanning inbox when looking for specific emails or threads.',
59
+ exampleUsage: "outlook_search({ query: 'quarterly review' }) to find meeting-related emails.",
60
+ returnHint: 'JSON array of matching email objects (same format as outlook_inbox).',
61
+ paginationNote: 'Default limit 10. Increase only if the user needs comprehensive results.',
62
+ },
63
+ outlook_send: {
64
+ description: 'Send an email. IMPORTANT: This actually sends — there is no undo. Always confirm with the user before sending unless you have explicit pre-approval (send policy). For drafting without sending, use outlook_draft.',
65
+ exampleUsage: 'Only use after user explicitly says "send it" or send policy allows autonomous sending.',
66
+ },
67
+ outlook_draft: {
68
+ description: 'Save an email as a draft without sending. Use this when the user wants to review before sending, or when preparing multiple emails for batch review.',
69
+ exampleUsage: 'Draft the email first, show the user, then send only after approval.',
70
+ },
71
+ github_prs: {
72
+ description: 'Check GitHub PRs requiring your review and your open PRs. Read-only overview. Requires gh CLI to be authenticated.',
73
+ returnHint: 'Two sections: PRs needing review + your open PRs, formatted as text.',
74
+ },
75
+ rss_fetch: {
76
+ description: 'Fetch and parse RSS feeds. If no URL provided, reads all enabled feeds from vault/00-System/RSS-FEEDS.md. Returns article titles, links, dates, and summaries.',
77
+ paginationNote: 'Each feed returns up to 10 articles. For many feeds, output can be large — consider processing feeds one at a time.',
78
+ },
79
+ web_search: {
80
+ description: 'Search the web via DuckDuckGo. Returns titles, URLs, and snippets. Use for current events, fact-checking, or research that vault notes cannot answer.',
81
+ exampleUsage: "web_search({ query: 'latest AI news this week' })",
82
+ returnHint: 'List of search results with title, URL, and snippet text.',
83
+ },
84
+ // ── Goals & Tasks ────────────────────────────────────────────────
85
+ task_list: {
86
+ description: 'List tasks from the master task list (TASKS.md). Returns task IDs, status, and descriptions. Filter by status to focus on pending or in-progress items.',
87
+ returnHint: 'Formatted task list with IDs like {T-001}, status, and description.',
88
+ },
89
+ task_add: {
90
+ description: 'Add a new task to the master list. Auto-generates a unique ID. Use for actionable items the user wants tracked.',
91
+ },
92
+ goal_create: {
93
+ description: 'Create a persistent goal with milestones and success criteria. Goals are long-running objectives that span multiple sessions. Use for multi-week projects or recurring objectives.',
94
+ },
95
+ goal_work: {
96
+ description: 'Spawn a focused work session on a specific goal. The sub-agent gets the goal context and works autonomously. Use when a goal needs dedicated attention.',
97
+ },
98
+ // ── Team & Agents ────────────────────────────────────────────────
99
+ team_message: {
100
+ description: 'Send a message to another agent on your team. The message is delivered to their active session or queued for later. Use for delegation, status updates, or requesting information from a specialist.',
101
+ exampleUsage: "team_message({ to: 'sdr-agent', message: 'Research Acme Corp for our call tomorrow' })",
102
+ },
103
+ delegate_task: {
104
+ description: 'Delegate a task to another agent with full context. More structured than team_message — includes objectives, expected output, and deadline. The delegated agent works asynchronously.',
105
+ },
106
+ // ── System ───────────────────────────────────────────────────────
107
+ self_restart: {
108
+ description: 'Restart the Clementine daemon. Use only when the user explicitly requests a restart or when a critical configuration change requires it.',
109
+ },
110
+ set_timer: {
111
+ description: 'Set a reminder timer that fires after a delay. The reminder message is delivered to the current session when it triggers.',
112
+ },
113
+ };
114
+ /**
115
+ * Get the enhanced description for a tool, or null if not defined.
116
+ * Falls back gracefully so existing inline descriptions still work.
117
+ */
118
+ export function getToolDescription(toolName) {
119
+ return TOOL_META[toolName]?.description ?? null;
120
+ }
121
+ /**
122
+ * Get full tool metadata for a tool.
123
+ */
124
+ export function getToolMeta(toolName) {
125
+ return TOOL_META[toolName] ?? null;
126
+ }
127
+ /**
128
+ * Get all tool names that have enhanced descriptions.
129
+ */
130
+ export function getEnhancedToolNames() {
131
+ return Object.keys(TOOL_META);
132
+ }
133
+ //# sourceMappingURL=tool-meta.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Clementine TypeScript — Vault & Notes MCP tools.
3
+ *
4
+ * note_create, task_list, task_add, task_update, vault_stats, daily_note, note_take
5
+ */
6
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ export declare function registerVaultTools(server: McpServer): void;
8
+ //# sourceMappingURL=vault-tools.d.ts.map