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.
- package/.env.example +44 -0
- package/LICENSE +21 -0
- package/README.md +795 -0
- package/dist/agent/agent-manager.d.ts +69 -0
- package/dist/agent/agent-manager.js +441 -0
- package/dist/agent/assistant.d.ts +225 -0
- package/dist/agent/assistant.js +3888 -0
- package/dist/agent/auto-update.d.ts +32 -0
- package/dist/agent/auto-update.js +186 -0
- package/dist/agent/daily-planner.d.ts +24 -0
- package/dist/agent/daily-planner.js +379 -0
- package/dist/agent/execution-advisor.d.ts +10 -0
- package/dist/agent/execution-advisor.js +272 -0
- package/dist/agent/hooks.d.ts +45 -0
- package/dist/agent/hooks.js +564 -0
- package/dist/agent/insight-engine.d.ts +66 -0
- package/dist/agent/insight-engine.js +225 -0
- package/dist/agent/intent-classifier.d.ts +48 -0
- package/dist/agent/intent-classifier.js +214 -0
- package/dist/agent/link-extractor.d.ts +19 -0
- package/dist/agent/link-extractor.js +90 -0
- package/dist/agent/mcp-bridge.d.ts +62 -0
- package/dist/agent/mcp-bridge.js +435 -0
- package/dist/agent/metacognition.d.ts +66 -0
- package/dist/agent/metacognition.js +221 -0
- package/dist/agent/orchestrator.d.ts +81 -0
- package/dist/agent/orchestrator.js +790 -0
- package/dist/agent/profiles.d.ts +22 -0
- package/dist/agent/profiles.js +91 -0
- package/dist/agent/prompt-cache.d.ts +24 -0
- package/dist/agent/prompt-cache.js +68 -0
- package/dist/agent/prompt-evolver.d.ts +28 -0
- package/dist/agent/prompt-evolver.js +279 -0
- package/dist/agent/role-scaffolds.d.ts +28 -0
- package/dist/agent/role-scaffolds.js +433 -0
- package/dist/agent/safe-restart.d.ts +41 -0
- package/dist/agent/safe-restart.js +150 -0
- package/dist/agent/self-improve.d.ts +66 -0
- package/dist/agent/self-improve.js +1706 -0
- package/dist/agent/session-event-log.d.ts +114 -0
- package/dist/agent/session-event-log.js +233 -0
- package/dist/agent/skill-extractor.d.ts +72 -0
- package/dist/agent/skill-extractor.js +435 -0
- package/dist/agent/source-mods.d.ts +61 -0
- package/dist/agent/source-mods.js +230 -0
- package/dist/agent/source-preflight.d.ts +25 -0
- package/dist/agent/source-preflight.js +100 -0
- package/dist/agent/stall-guard.d.ts +62 -0
- package/dist/agent/stall-guard.js +109 -0
- package/dist/agent/strategic-planner.d.ts +60 -0
- package/dist/agent/strategic-planner.js +352 -0
- package/dist/agent/team-bus.d.ts +89 -0
- package/dist/agent/team-bus.js +556 -0
- package/dist/agent/team-router.d.ts +26 -0
- package/dist/agent/team-router.js +37 -0
- package/dist/agent/tool-loop-detector.d.ts +59 -0
- package/dist/agent/tool-loop-detector.js +242 -0
- package/dist/agent/workflow-runner.d.ts +36 -0
- package/dist/agent/workflow-runner.js +317 -0
- package/dist/agent/workflow-variables.d.ts +16 -0
- package/dist/agent/workflow-variables.js +62 -0
- package/dist/channels/discord-agent-bot.d.ts +101 -0
- package/dist/channels/discord-agent-bot.js +881 -0
- package/dist/channels/discord-bot-manager.d.ts +80 -0
- package/dist/channels/discord-bot-manager.js +262 -0
- package/dist/channels/discord-utils.d.ts +51 -0
- package/dist/channels/discord-utils.js +293 -0
- package/dist/channels/discord.d.ts +12 -0
- package/dist/channels/discord.js +1832 -0
- package/dist/channels/slack-agent-bot.d.ts +73 -0
- package/dist/channels/slack-agent-bot.js +320 -0
- package/dist/channels/slack-bot-manager.d.ts +66 -0
- package/dist/channels/slack-bot-manager.js +236 -0
- package/dist/channels/slack-utils.d.ts +39 -0
- package/dist/channels/slack-utils.js +189 -0
- package/dist/channels/slack.d.ts +11 -0
- package/dist/channels/slack.js +196 -0
- package/dist/channels/telegram.d.ts +10 -0
- package/dist/channels/telegram.js +235 -0
- package/dist/channels/webhook.d.ts +9 -0
- package/dist/channels/webhook.js +78 -0
- package/dist/channels/whatsapp.d.ts +11 -0
- package/dist/channels/whatsapp.js +181 -0
- package/dist/cli/chat.d.ts +14 -0
- package/dist/cli/chat.js +220 -0
- package/dist/cli/cron.d.ts +17 -0
- package/dist/cli/cron.js +552 -0
- package/dist/cli/dashboard.d.ts +15 -0
- package/dist/cli/dashboard.js +17677 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +2474 -0
- package/dist/cli/routes/delegations.d.ts +19 -0
- package/dist/cli/routes/delegations.js +154 -0
- package/dist/cli/routes/digest.d.ts +17 -0
- package/dist/cli/routes/digest.js +375 -0
- package/dist/cli/routes/goals.d.ts +14 -0
- package/dist/cli/routes/goals.js +258 -0
- package/dist/cli/routes/workflows.d.ts +18 -0
- package/dist/cli/routes/workflows.js +97 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +619 -0
- package/dist/cli/tunnel.d.ts +35 -0
- package/dist/cli/tunnel.js +141 -0
- package/dist/config.d.ts +145 -0
- package/dist/config.js +278 -0
- package/dist/events/bus.d.ts +43 -0
- package/dist/events/bus.js +136 -0
- package/dist/gateway/cron-scheduler.d.ts +166 -0
- package/dist/gateway/cron-scheduler.js +1767 -0
- package/dist/gateway/delivery-queue.d.ts +30 -0
- package/dist/gateway/delivery-queue.js +110 -0
- package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
- package/dist/gateway/heartbeat-scheduler.js +1298 -0
- package/dist/gateway/heartbeat.d.ts +3 -0
- package/dist/gateway/heartbeat.js +3 -0
- package/dist/gateway/lanes.d.ts +24 -0
- package/dist/gateway/lanes.js +76 -0
- package/dist/gateway/notifications.d.ts +29 -0
- package/dist/gateway/notifications.js +75 -0
- package/dist/gateway/router.d.ts +210 -0
- package/dist/gateway/router.js +1330 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1015 -0
- package/dist/memory/chunker.d.ts +28 -0
- package/dist/memory/chunker.js +226 -0
- package/dist/memory/consolidation.d.ts +44 -0
- package/dist/memory/consolidation.js +171 -0
- package/dist/memory/context-assembler.d.ts +50 -0
- package/dist/memory/context-assembler.js +149 -0
- package/dist/memory/embeddings.d.ts +38 -0
- package/dist/memory/embeddings.js +180 -0
- package/dist/memory/graph-store.d.ts +66 -0
- package/dist/memory/graph-store.js +613 -0
- package/dist/memory/mmr.d.ts +21 -0
- package/dist/memory/mmr.js +75 -0
- package/dist/memory/search.d.ts +26 -0
- package/dist/memory/search.js +67 -0
- package/dist/memory/store.d.ts +530 -0
- package/dist/memory/store.js +2022 -0
- package/dist/security/integrity.d.ts +24 -0
- package/dist/security/integrity.js +58 -0
- package/dist/security/patterns.d.ts +34 -0
- package/dist/security/patterns.js +110 -0
- package/dist/security/scanner.d.ts +32 -0
- package/dist/security/scanner.js +263 -0
- package/dist/tools/admin-tools.d.ts +12 -0
- package/dist/tools/admin-tools.js +1278 -0
- package/dist/tools/external-tools.d.ts +11 -0
- package/dist/tools/external-tools.js +1327 -0
- package/dist/tools/goal-tools.d.ts +9 -0
- package/dist/tools/goal-tools.js +159 -0
- package/dist/tools/mcp-server.d.ts +13 -0
- package/dist/tools/mcp-server.js +141 -0
- package/dist/tools/memory-tools.d.ts +10 -0
- package/dist/tools/memory-tools.js +568 -0
- package/dist/tools/session-tools.d.ts +6 -0
- package/dist/tools/session-tools.js +146 -0
- package/dist/tools/shared.d.ts +216 -0
- package/dist/tools/shared.js +340 -0
- package/dist/tools/team-tools.d.ts +6 -0
- package/dist/tools/team-tools.js +447 -0
- package/dist/tools/tool-meta.d.ts +34 -0
- package/dist/tools/tool-meta.js +133 -0
- package/dist/tools/vault-tools.d.ts +8 -0
- package/dist/tools/vault-tools.js +457 -0
- package/dist/types.d.ts +716 -0
- package/dist/types.js +16 -0
- package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
- package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
- package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
- package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
- package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
- package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
- package/dist/vault-migrations/helpers.d.ts +14 -0
- package/dist/vault-migrations/helpers.js +44 -0
- package/dist/vault-migrations/runner.d.ts +14 -0
- package/dist/vault-migrations/runner.js +139 -0
- package/dist/vault-migrations/types.d.ts +42 -0
- package/dist/vault-migrations/types.js +9 -0
- package/install.sh +320 -0
- package/package.json +84 -0
- package/scripts/postinstall.js +125 -0
- package/vault/00-System/AGENTS.md +66 -0
- package/vault/00-System/CRON.md +71 -0
- package/vault/00-System/HEARTBEAT.md +58 -0
- package/vault/00-System/MEMORY.md +16 -0
- package/vault/00-System/SOUL.md +96 -0
- package/vault/05-Tasks/TASKS.md +19 -0
- package/vault/06-Templates/_Daily-Template.md +28 -0
- package/vault/06-Templates/_People-Template.md +22 -0
|
@@ -0,0 +1,457 @@
|
|
|
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 { randomBytes } from 'node:crypto';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { ACTIVE_AGENT_SLUG, BASE_DIR, DAILY_NOTES_DIR, INBOX_DIR, MEMORY_FILE, PEOPLE_DIR, PROJECTS_DIR, SYSTEM_DIR, TASKS_DIR, TASKS_FILE, TEMPLATES_DIR, TOPICS_DIR, VAULT_DIR, TASK_ID_RE, agentTasksFile, agentDailyNotesDir, ensureDailyNote, folderForType, getStore, globMd, incrementalSync, nextDueDate, nextTaskId, nowTime, parseTasks, textResult, timeOfDaySection, todayStr, validateVaultPath, } from './shared.js';
|
|
11
|
+
export function registerVaultTools(server) {
|
|
12
|
+
// ── 5. note_create ─────────────────────────────────────────────────────
|
|
13
|
+
server.tool('note_create', 'Create a new note in the right vault folder. Types: person, project, topic, task, inbox.', {
|
|
14
|
+
note_type: z.enum(['person', 'project', 'topic', 'task', 'inbox']).describe('Note type'),
|
|
15
|
+
title: z.string().describe('Note title'),
|
|
16
|
+
content: z.string().optional().describe('Initial body content'),
|
|
17
|
+
}, async ({ note_type, title, content }) => {
|
|
18
|
+
const folder = folderForType(note_type);
|
|
19
|
+
mkdirSync(folder, { recursive: true });
|
|
20
|
+
const safe = title.replace(/[<>:"/\\|?*]/g, '');
|
|
21
|
+
const notePath = path.join(folder, `${safe}.md`);
|
|
22
|
+
const relPath = path.relative(VAULT_DIR, notePath);
|
|
23
|
+
validateVaultPath(relPath);
|
|
24
|
+
if (existsSync(notePath)) {
|
|
25
|
+
return textResult(`Already exists: ${relPath}`);
|
|
26
|
+
}
|
|
27
|
+
// Dedup check for note content — bump salience instead of discarding
|
|
28
|
+
if (content && content.length >= 20) {
|
|
29
|
+
try {
|
|
30
|
+
const store = await getStore();
|
|
31
|
+
const dup = store.checkDuplicate(content);
|
|
32
|
+
if (dup.isDuplicate && dup.matchId) {
|
|
33
|
+
store.bumpChunkSalience(dup.matchId, 0.1);
|
|
34
|
+
store.logExtraction({
|
|
35
|
+
sessionKey: 'mcp', userMessage: `note_create: ${title}`,
|
|
36
|
+
toolName: 'note_create', toolInput: JSON.stringify({ note_type, title }),
|
|
37
|
+
extractedAt: new Date().toISOString(), status: 'dedup_skipped',
|
|
38
|
+
});
|
|
39
|
+
return textResult(`Reinforced existing memory (chunk #${dup.matchId}, salience bumped). No duplicate note created.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { /* dedup failure is non-fatal */ }
|
|
43
|
+
}
|
|
44
|
+
const body = content ?? `# ${title}\n`;
|
|
45
|
+
const noteContent = `---
|
|
46
|
+
type: ${note_type}
|
|
47
|
+
created: "${todayStr()}"
|
|
48
|
+
tags:
|
|
49
|
+
- ${note_type}
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
${body}
|
|
53
|
+
`;
|
|
54
|
+
writeFileSync(notePath, noteContent, 'utf-8');
|
|
55
|
+
await incrementalSync(relPath);
|
|
56
|
+
return textResult(`Created [[${safe}]] at ${relPath}`);
|
|
57
|
+
});
|
|
58
|
+
// ── 6. task_list ───────────────────────────────────────────────────────
|
|
59
|
+
server.tool('task_list', 'List tasks from the master task list. Tasks have IDs like {T-001}. Tasks may have @assignee:agentname tags — use assignee filter to see only tasks for a specific agent.', {
|
|
60
|
+
status: z.enum(['all', 'pending', 'completed']).optional().describe('Filter by status'),
|
|
61
|
+
project: z.string().optional().describe('Filter by project tag'),
|
|
62
|
+
assignee: z.string().optional().describe('Filter by assignee (e.g. "ross-the-sdr", "nora-senior-sdr", "clementine"). Use "unassigned" to see tasks with no assignee.'),
|
|
63
|
+
}, async ({ status, project, assignee }) => {
|
|
64
|
+
const statusFilter = status ?? 'all';
|
|
65
|
+
const projectFilter = project ?? '';
|
|
66
|
+
const assigneeFilter = assignee ?? '';
|
|
67
|
+
const tasksFilePath = agentTasksFile(ACTIVE_AGENT_SLUG);
|
|
68
|
+
if (!existsSync(tasksFilePath)) {
|
|
69
|
+
return textResult('No task list found.');
|
|
70
|
+
}
|
|
71
|
+
const body = readFileSync(tasksFilePath, 'utf-8');
|
|
72
|
+
const allTasks = parseTasks(body);
|
|
73
|
+
let filtered = allTasks;
|
|
74
|
+
if (statusFilter !== 'all') {
|
|
75
|
+
filtered = filtered.filter(t => t.status === statusFilter);
|
|
76
|
+
}
|
|
77
|
+
if (projectFilter) {
|
|
78
|
+
filtered = filtered.filter(t => t.project.toLowerCase() === projectFilter.toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
if (assigneeFilter) {
|
|
81
|
+
if (assigneeFilter === 'unassigned') {
|
|
82
|
+
filtered = filtered.filter(t => !t.assignee);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
filtered = filtered.filter(t => t.assignee.toLowerCase() === assigneeFilter.toLowerCase());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!filtered.length) {
|
|
89
|
+
const parts = [statusFilter];
|
|
90
|
+
if (projectFilter)
|
|
91
|
+
parts.push(`project:${projectFilter}`);
|
|
92
|
+
if (assigneeFilter)
|
|
93
|
+
parts.push(`assignee:${assigneeFilter}`);
|
|
94
|
+
return textResult(`No tasks matching: ${parts.join(', ')}`);
|
|
95
|
+
}
|
|
96
|
+
// Annotate each line with assignee if present
|
|
97
|
+
const lines = filtered.map(t => {
|
|
98
|
+
const assigneeNote = t.assignee ? ` [assignee: ${t.assignee}]` : '';
|
|
99
|
+
return t.rawLine + assigneeNote;
|
|
100
|
+
});
|
|
101
|
+
let header = `**Tasks (${statusFilter})`;
|
|
102
|
+
if (projectFilter)
|
|
103
|
+
header += `, project:${projectFilter}`;
|
|
104
|
+
if (assigneeFilter)
|
|
105
|
+
header += `, assignee:${assigneeFilter}`;
|
|
106
|
+
header += ` — ${filtered.length} results:**`;
|
|
107
|
+
return textResult(`${header}\n\n${lines.join('\n')}`);
|
|
108
|
+
});
|
|
109
|
+
// ── 7. task_add ────────────────────────────────────────────────────────
|
|
110
|
+
server.tool('task_add', 'Add a new task to the master task list. Auto-generates a {T-NNN} ID. Include @assignee:agentname in description to assign to a specific agent (e.g. @assignee:ross-the-sdr).', {
|
|
111
|
+
description: z.string().describe('Task description. Include @assignee:agentname to assign to a specific agent.'),
|
|
112
|
+
priority: z.enum(['high', 'medium', 'low']).optional().describe('Task priority'),
|
|
113
|
+
due_date: z.string().optional().describe('Due date (YYYY-MM-DD)'),
|
|
114
|
+
project: z.string().optional().describe('Project name'),
|
|
115
|
+
}, async ({ description, priority, due_date, project }) => {
|
|
116
|
+
// Dedup check for task descriptions
|
|
117
|
+
if (description.length >= 20) {
|
|
118
|
+
try {
|
|
119
|
+
const store = await getStore();
|
|
120
|
+
const dup = store.checkDuplicate(description);
|
|
121
|
+
if (dup.isDuplicate) {
|
|
122
|
+
store.logExtraction({
|
|
123
|
+
sessionKey: 'mcp', userMessage: description.slice(0, 200),
|
|
124
|
+
toolName: 'task_add', toolInput: JSON.stringify({ description, project }),
|
|
125
|
+
extractedAt: new Date().toISOString(), status: 'dedup_skipped',
|
|
126
|
+
});
|
|
127
|
+
return textResult(`Skipped: ${dup.matchType} duplicate task already exists (chunk #${dup.matchId})`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch { /* dedup failure is non-fatal */ }
|
|
131
|
+
}
|
|
132
|
+
const tasksFilePath = agentTasksFile(ACTIVE_AGENT_SLUG);
|
|
133
|
+
if (!existsSync(tasksFilePath)) {
|
|
134
|
+
mkdirSync(path.dirname(tasksFilePath), { recursive: true });
|
|
135
|
+
writeFileSync(tasksFilePath, `---\ntype: task-list\ntags:\n - tasks\n---\n\n# Tasks\n\n## Pending\n\n## In Progress\n\n## Completed\n`, 'utf-8');
|
|
136
|
+
}
|
|
137
|
+
let body = readFileSync(tasksFilePath, 'utf-8');
|
|
138
|
+
const taskId = nextTaskId(body);
|
|
139
|
+
// Build metadata suffix
|
|
140
|
+
let meta = '';
|
|
141
|
+
if (priority && priority !== 'medium') {
|
|
142
|
+
meta += ` !!${priority}`;
|
|
143
|
+
}
|
|
144
|
+
if (due_date) {
|
|
145
|
+
meta += ` 📅 ${due_date}`;
|
|
146
|
+
}
|
|
147
|
+
if (project) {
|
|
148
|
+
meta += ` #project:${project}`;
|
|
149
|
+
}
|
|
150
|
+
const taskLine = `- [ ] {${taskId}} ${description}${meta}`;
|
|
151
|
+
const pendingMatch = /## Pending\n/.exec(body);
|
|
152
|
+
if (pendingMatch) {
|
|
153
|
+
const insertPos = pendingMatch.index + pendingMatch[0].length;
|
|
154
|
+
body = body.slice(0, insertPos) + `\n${taskLine}` + body.slice(insertPos);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
body += `\n## Pending\n\n${taskLine}\n`;
|
|
158
|
+
}
|
|
159
|
+
writeFileSync(tasksFilePath, body, 'utf-8');
|
|
160
|
+
const rel = path.relative(VAULT_DIR, tasksFilePath);
|
|
161
|
+
await incrementalSync(rel);
|
|
162
|
+
return textResult(`Added task {${taskId}}: ${description}`);
|
|
163
|
+
});
|
|
164
|
+
// ── 8. task_update ─────────────────────────────────────────────────────
|
|
165
|
+
server.tool('task_update', "Update a task's status or metadata by {T-NNN} ID.", {
|
|
166
|
+
task_id: z.string().describe('Task ID like T-001'),
|
|
167
|
+
status: z.enum(['pending', 'completed']).optional().describe('New status'),
|
|
168
|
+
description: z.string().optional().describe('New description text'),
|
|
169
|
+
priority: z.string().optional().describe('New priority'),
|
|
170
|
+
due_date: z.string().optional().describe('New due date (YYYY-MM-DD)'),
|
|
171
|
+
}, async ({ task_id, status, description: _newDesc, priority: newPriority, due_date: newDue }) => {
|
|
172
|
+
const tasksFilePath = agentTasksFile(ACTIVE_AGENT_SLUG);
|
|
173
|
+
if (!existsSync(tasksFilePath)) {
|
|
174
|
+
return textResult('No task list found.');
|
|
175
|
+
}
|
|
176
|
+
let body = readFileSync(tasksFilePath, 'utf-8');
|
|
177
|
+
const lines = body.split('\n');
|
|
178
|
+
// Normalize task ID
|
|
179
|
+
let taskIdClean = task_id.replace(/[{}]/g, '');
|
|
180
|
+
if (!taskIdClean.startsWith('T-'))
|
|
181
|
+
taskIdClean = `T-${taskIdClean}`;
|
|
182
|
+
const searchPattern = `{${taskIdClean}}`;
|
|
183
|
+
// Find the task line
|
|
184
|
+
let foundIdx = null;
|
|
185
|
+
let foundLine = '';
|
|
186
|
+
for (let i = 0; i < lines.length; i++) {
|
|
187
|
+
if (/^\s*- \[[ xX]\]/.test(lines[i]) && lines[i].includes(searchPattern)) {
|
|
188
|
+
foundIdx = i;
|
|
189
|
+
foundLine = lines[i].trim();
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (foundIdx === null) {
|
|
194
|
+
return textResult(`Task not found: ${task_id}`);
|
|
195
|
+
}
|
|
196
|
+
// Check for recurrence before modifying
|
|
197
|
+
const recMatch = /🔁\s*(\S+)/.exec(foundLine);
|
|
198
|
+
const dueMatch = /📅\s*(\d{4}-\d{2}-\d{2})/.exec(foundLine);
|
|
199
|
+
// Apply metadata changes
|
|
200
|
+
if (newPriority) {
|
|
201
|
+
if (/!!(low|normal|high|urgent)/.test(foundLine)) {
|
|
202
|
+
foundLine = foundLine.replace(/!!(low|normal|high|urgent)/, `!!${newPriority}`);
|
|
203
|
+
}
|
|
204
|
+
else if (newPriority !== 'normal') {
|
|
205
|
+
const idM = TASK_ID_RE.exec(foundLine);
|
|
206
|
+
if (idM) {
|
|
207
|
+
const pos = (idM.index ?? 0) + idM[0].length;
|
|
208
|
+
foundLine = foundLine.slice(0, pos) + ` !!${newPriority}` + foundLine.slice(pos);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (newDue) {
|
|
213
|
+
if (/📅\s*\d{4}-\d{2}-\d{2}/.test(foundLine)) {
|
|
214
|
+
foundLine = foundLine.replace(/📅\s*\d{4}-\d{2}-\d{2}/, `📅 ${newDue}`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
foundLine += ` 📅 ${newDue}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Update checkbox
|
|
221
|
+
const newStatus = status ?? 'pending';
|
|
222
|
+
lines.splice(foundIdx, 1);
|
|
223
|
+
if (newStatus === 'completed') {
|
|
224
|
+
foundLine = foundLine.replace(/- \[ \]/, '- [x]');
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
foundLine = foundLine.replace(/- \[[xX]\]/, '- [ ]');
|
|
228
|
+
}
|
|
229
|
+
// Move to the right section
|
|
230
|
+
const headers = {
|
|
231
|
+
pending: '## Pending',
|
|
232
|
+
'in-progress': '## In Progress',
|
|
233
|
+
completed: '## Completed',
|
|
234
|
+
};
|
|
235
|
+
const target = headers[newStatus] ?? '## Pending';
|
|
236
|
+
for (let i = 0; i < lines.length; i++) {
|
|
237
|
+
if (lines[i].trim() === target) {
|
|
238
|
+
let insertAt = i + 1;
|
|
239
|
+
if (insertAt < lines.length && lines[insertAt].trim() === '')
|
|
240
|
+
insertAt++;
|
|
241
|
+
// Remove placeholder if present
|
|
242
|
+
if (insertAt < lines.length && lines[insertAt].trim().startsWith('*(')) {
|
|
243
|
+
lines.splice(insertAt, 1);
|
|
244
|
+
}
|
|
245
|
+
lines.splice(insertAt, 0, foundLine);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
body = lines.join('\n');
|
|
250
|
+
// Handle recurring task: create new copy with next due date
|
|
251
|
+
let recurringMsg = '';
|
|
252
|
+
if (newStatus === 'completed' && recMatch && dueMatch) {
|
|
253
|
+
const recurrence = recMatch[1];
|
|
254
|
+
const currentDue = dueMatch[1];
|
|
255
|
+
const nextDue = nextDueDate(currentDue, recurrence);
|
|
256
|
+
const newId = nextTaskId(body);
|
|
257
|
+
let newLine = foundLine;
|
|
258
|
+
newLine = newLine.replace(/- \[[xX]\]/, '- [ ]');
|
|
259
|
+
newLine = newLine.replace(TASK_ID_RE, `{${newId}}`);
|
|
260
|
+
newLine = newLine.replace(/📅\s*\d{4}-\d{2}-\d{2}/, `📅 ${nextDue}`);
|
|
261
|
+
const pMatch = /## Pending\n/.exec(body);
|
|
262
|
+
if (pMatch) {
|
|
263
|
+
const insertPos = pMatch.index + pMatch[0].length;
|
|
264
|
+
body = body.slice(0, insertPos) + `\n${newLine}` + body.slice(insertPos);
|
|
265
|
+
}
|
|
266
|
+
recurringMsg = ` | Next occurrence {${newId}} due ${nextDue}`;
|
|
267
|
+
}
|
|
268
|
+
writeFileSync(tasksFilePath, body, 'utf-8');
|
|
269
|
+
const rel = path.relative(VAULT_DIR, tasksFilePath);
|
|
270
|
+
await incrementalSync(rel);
|
|
271
|
+
return textResult(`Moved to ${newStatus}: ${task_id}${recurringMsg}`);
|
|
272
|
+
});
|
|
273
|
+
// ── 8b. heartbeat_queue_work ──────────────────────────────────────────
|
|
274
|
+
const HEARTBEAT_WORK_QUEUE_FILE = path.join(BASE_DIR, 'heartbeat', 'work-queue.json');
|
|
275
|
+
server.tool('heartbeat_queue_work', 'Queue a background task for the next heartbeat cycle to execute. Use for approved work that should happen asynchronously during the next check-in.', {
|
|
276
|
+
description: z.string().describe('Short human-readable description of the work'),
|
|
277
|
+
prompt: z.string().describe('Detailed prompt for the agent executing this work'),
|
|
278
|
+
priority: z.enum(['high', 'normal']).optional().default('normal').describe('high = next tick, normal = when convenient'),
|
|
279
|
+
max_turns: z.number().optional().default(3).describe('Max conversation turns for this work (1-5)'),
|
|
280
|
+
tier: z.number().optional().default(1).describe('Security tier: 1 = vault-only, 2 = bash/git allowed'),
|
|
281
|
+
agent: z.string().optional().describe('Agent slug this work is for (e.g. "ross"). Omit for global work.'),
|
|
282
|
+
}, async ({ description, prompt, priority, max_turns, tier, agent }) => {
|
|
283
|
+
const queueDir = path.dirname(HEARTBEAT_WORK_QUEUE_FILE);
|
|
284
|
+
mkdirSync(queueDir, { recursive: true });
|
|
285
|
+
let queue = [];
|
|
286
|
+
try {
|
|
287
|
+
if (existsSync(HEARTBEAT_WORK_QUEUE_FILE)) {
|
|
288
|
+
queue = JSON.parse(readFileSync(HEARTBEAT_WORK_QUEUE_FILE, 'utf-8'));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch { /* start fresh */ }
|
|
292
|
+
const id = randomBytes(4).toString('hex');
|
|
293
|
+
const item = {
|
|
294
|
+
id,
|
|
295
|
+
description,
|
|
296
|
+
prompt,
|
|
297
|
+
source: 'mcp-tool',
|
|
298
|
+
priority,
|
|
299
|
+
queuedAt: new Date().toISOString(),
|
|
300
|
+
maxTurns: Math.min(max_turns, 5),
|
|
301
|
+
tier: Math.min(tier, 2),
|
|
302
|
+
status: 'pending',
|
|
303
|
+
};
|
|
304
|
+
if (agent)
|
|
305
|
+
item.agentSlug = agent;
|
|
306
|
+
queue.push(item);
|
|
307
|
+
writeFileSync(HEARTBEAT_WORK_QUEUE_FILE, JSON.stringify(queue, null, 2));
|
|
308
|
+
return textResult(`Queued work item ${id}: "${description}" (priority: ${priority}, next heartbeat will pick it up)`);
|
|
309
|
+
});
|
|
310
|
+
// ── 9. vault_stats ─────────────────────────────────────────────────────
|
|
311
|
+
server.tool('vault_stats', 'Quick dashboard of vault health — note counts, task counts, memory size, recent activity.', { _empty: z.string().optional().describe('(no parameters needed)') }, async () => {
|
|
312
|
+
const lines = ['**Vault Dashboard:**\n'];
|
|
313
|
+
// Note counts by folder
|
|
314
|
+
const folders = [
|
|
315
|
+
SYSTEM_DIR, DAILY_NOTES_DIR, PEOPLE_DIR, PROJECTS_DIR,
|
|
316
|
+
TOPICS_DIR, TASKS_DIR, TEMPLATES_DIR, INBOX_DIR,
|
|
317
|
+
];
|
|
318
|
+
lines.push('**Notes by folder:**');
|
|
319
|
+
for (const folder of folders) {
|
|
320
|
+
if (existsSync(folder)) {
|
|
321
|
+
try {
|
|
322
|
+
const count = readdirSync(folder).filter(f => f.endsWith('.md')).length;
|
|
323
|
+
lines.push(` - ${path.basename(folder)}: ${count}`);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// skip
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Task counts
|
|
331
|
+
if (existsSync(TASKS_FILE)) {
|
|
332
|
+
const body = readFileSync(TASKS_FILE, 'utf-8');
|
|
333
|
+
const tasks = parseTasks(body);
|
|
334
|
+
const statusCounts = {};
|
|
335
|
+
let overdue = 0;
|
|
336
|
+
const today = todayStr();
|
|
337
|
+
for (const t of tasks) {
|
|
338
|
+
statusCounts[t.status] = (statusCounts[t.status] ?? 0) + 1;
|
|
339
|
+
if (t.due && t.due < today && !t.checked)
|
|
340
|
+
overdue++;
|
|
341
|
+
}
|
|
342
|
+
lines.push('\n**Tasks:**');
|
|
343
|
+
for (const [st, count] of Object.entries(statusCounts).sort()) {
|
|
344
|
+
lines.push(` - ${st}: ${count}`);
|
|
345
|
+
}
|
|
346
|
+
if (overdue)
|
|
347
|
+
lines.push(` - **OVERDUE: ${overdue}**`);
|
|
348
|
+
}
|
|
349
|
+
// MEMORY.md size
|
|
350
|
+
if (existsSync(MEMORY_FILE)) {
|
|
351
|
+
const memContent = readFileSync(MEMORY_FILE, 'utf-8');
|
|
352
|
+
const memLines = memContent.split('\n').length;
|
|
353
|
+
const memChars = memContent.length;
|
|
354
|
+
lines.push(`\n**MEMORY.md:** ${memLines} lines, ${memChars.toLocaleString()} chars`);
|
|
355
|
+
}
|
|
356
|
+
// 5 most recently modified notes
|
|
357
|
+
const allNotes = globMd(VAULT_DIR)
|
|
358
|
+
.filter(f => !f.includes('06-Templates'))
|
|
359
|
+
.map(f => ({ path: f, mtime: statSync(f).mtimeMs }))
|
|
360
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
361
|
+
.slice(0, 5);
|
|
362
|
+
if (allNotes.length) {
|
|
363
|
+
lines.push('\n**Recently modified:**');
|
|
364
|
+
for (const note of allNotes) {
|
|
365
|
+
const rel = path.relative(VAULT_DIR, note.path);
|
|
366
|
+
const mtime = new Date(note.mtime).toISOString().slice(0, 16).replace('T', ' ');
|
|
367
|
+
lines.push(` - ${rel} (${mtime})`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Inbox count
|
|
371
|
+
if (existsSync(INBOX_DIR)) {
|
|
372
|
+
try {
|
|
373
|
+
const inboxCount = readdirSync(INBOX_DIR).filter(f => f.endsWith('.md')).length;
|
|
374
|
+
lines.push(`\n**Inbox items:** ${inboxCount}`);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// skip
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Chunk count from store
|
|
381
|
+
try {
|
|
382
|
+
const store = await getStore();
|
|
383
|
+
const db = store.db;
|
|
384
|
+
const row = db.prepare('SELECT COUNT(*) as cnt FROM chunks').get();
|
|
385
|
+
if (row)
|
|
386
|
+
lines.push(`\n**Indexed chunks:** ${row.cnt}`);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// store may not be initialized
|
|
390
|
+
}
|
|
391
|
+
return textResult(lines.join('\n'));
|
|
392
|
+
});
|
|
393
|
+
// ── 13. daily_note ─────────────────────────────────────────────────────
|
|
394
|
+
server.tool('daily_note', "Create or read today's daily note.", {
|
|
395
|
+
action: z.enum(['read', 'create']).optional().describe("'read' or 'create' (default: read)"),
|
|
396
|
+
}, async ({ action }) => {
|
|
397
|
+
const act = action ?? 'read';
|
|
398
|
+
if (act === 'create') {
|
|
399
|
+
const notePath = ensureDailyNote();
|
|
400
|
+
const rel = path.relative(VAULT_DIR, notePath);
|
|
401
|
+
await incrementalSync(rel);
|
|
402
|
+
return textResult(`Daily note ready: ${rel}`);
|
|
403
|
+
}
|
|
404
|
+
// read
|
|
405
|
+
const notePath = path.join(DAILY_NOTES_DIR, `${todayStr()}.md`);
|
|
406
|
+
if (!existsSync(notePath)) {
|
|
407
|
+
return textResult(`No daily note for today (${todayStr()}). Use action 'create' to create one.`);
|
|
408
|
+
}
|
|
409
|
+
const content = readFileSync(notePath, 'utf-8');
|
|
410
|
+
return textResult(`**${todayStr()}.md:**\n\n${content}`);
|
|
411
|
+
});
|
|
412
|
+
// ── 12. note_take ──────────────────────────────────────────────────────
|
|
413
|
+
server.tool('note_take', "Quick capture a timestamped note to today's daily log.", {
|
|
414
|
+
text: z.string().describe('Note text'),
|
|
415
|
+
}, async ({ text }) => {
|
|
416
|
+
const section = timeOfDaySection();
|
|
417
|
+
const dailyPath = ensureDailyNote();
|
|
418
|
+
let body = readFileSync(dailyPath, 'utf-8');
|
|
419
|
+
const timestamp = nowTime();
|
|
420
|
+
const entry = `\n- **${timestamp}** — ${text}`;
|
|
421
|
+
const escapedSection = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
422
|
+
const pattern = new RegExp(`(## ${escapedSection}.*?)(\\n## |$)`, 's');
|
|
423
|
+
const match = pattern.exec(body);
|
|
424
|
+
if (match) {
|
|
425
|
+
body = body.slice(0, match.index + match[1].length) + entry + body.slice(match.index + match[1].length);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
body += `\n\n## ${section}${entry}`;
|
|
429
|
+
}
|
|
430
|
+
writeFileSync(dailyPath, body, 'utf-8');
|
|
431
|
+
const rel = path.relative(VAULT_DIR, dailyPath);
|
|
432
|
+
await incrementalSync(rel);
|
|
433
|
+
return textResult(`Noted in ${path.basename(dailyPath)} > ${section}`);
|
|
434
|
+
});
|
|
435
|
+
// ── agent_daily_note ───────────────────────────────────────────────────
|
|
436
|
+
server.tool('agent_daily_note', 'Write an entry to today\'s daily note. When running as a team agent, writes to their own daily notes directory. Use this to log completed work, observations, and status updates.', {
|
|
437
|
+
content: z.string().describe('Content to append to today\'s daily note'),
|
|
438
|
+
replace: z.boolean().optional().describe('If true, replace today\'s note entirely instead of appending'),
|
|
439
|
+
}, async ({ content, replace }) => {
|
|
440
|
+
const { todayISO } = await import('../gateway/cron-scheduler.js');
|
|
441
|
+
const notesDir = agentDailyNotesDir(ACTIVE_AGENT_SLUG);
|
|
442
|
+
if (!existsSync(notesDir))
|
|
443
|
+
mkdirSync(notesDir, { recursive: true });
|
|
444
|
+
const today = todayISO();
|
|
445
|
+
const notePath = path.join(notesDir, `${today}.md`);
|
|
446
|
+
const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
447
|
+
if (replace || !existsSync(notePath)) {
|
|
448
|
+
writeFileSync(notePath, `# Daily Log — ${today}\n\n${content}\n`);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
const existing = readFileSync(notePath, 'utf-8');
|
|
452
|
+
writeFileSync(notePath, `${existing.trimEnd()}\n\n**${timestamp}:** ${content}\n`);
|
|
453
|
+
}
|
|
454
|
+
return textResult(`Daily note updated for ${today}.`);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
//# sourceMappingURL=vault-tools.js.map
|