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,568 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Memory MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* working_memory, memory_read/write/search/recall, memory_connections,
|
|
5
|
+
* memory_timeline, transcript_search, memory_report/correct/consolidate,
|
|
6
|
+
* graph memory tools
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { ACTIVE_AGENT_SLUG, BASE_DIR, IDENTITY_FILE, MEMORY_FILE, SYSTEM_DIR, VAULT_DIR, WORKING_MEMORY_MAX_LINES, agentWorkingMemoryFile, ensureDailyNote, getStore, globMd, incrementalSync, logger, nowTime, resolvePath, textResult, todayStr, validateVaultPath, } from './shared.js';
|
|
12
|
+
import { getToolDescription } from './tool-meta.js';
|
|
13
|
+
/** Merge duplicate `## Section` headers in a MEMORY.md body, deduplicating lines. */
|
|
14
|
+
function mergeDuplicateSections(body) {
|
|
15
|
+
const lines = body.split('\n');
|
|
16
|
+
const sections = new Map(); // heading → content lines
|
|
17
|
+
const order = []; // preserve first-seen order
|
|
18
|
+
let preamble = [];
|
|
19
|
+
let currentHeading = '';
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
if (line.startsWith('## ')) {
|
|
22
|
+
currentHeading = line;
|
|
23
|
+
if (!sections.has(currentHeading)) {
|
|
24
|
+
sections.set(currentHeading, []);
|
|
25
|
+
order.push(currentHeading);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (!currentHeading) {
|
|
29
|
+
preamble.push(line);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
sections.get(currentHeading).push(line);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (sections.size === 0)
|
|
36
|
+
return body; // no sections, nothing to merge
|
|
37
|
+
// Deduplicate lines within each section
|
|
38
|
+
for (const [heading, contentLines] of sections) {
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const deduped = [];
|
|
41
|
+
for (const line of contentLines) {
|
|
42
|
+
const key = line.trim().toLowerCase();
|
|
43
|
+
if (!key || key === '') {
|
|
44
|
+
deduped.push(line);
|
|
45
|
+
continue;
|
|
46
|
+
} // keep blank lines
|
|
47
|
+
if (!seen.has(key)) {
|
|
48
|
+
seen.add(key);
|
|
49
|
+
deduped.push(line);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
sections.set(heading, deduped);
|
|
53
|
+
}
|
|
54
|
+
// Rebuild
|
|
55
|
+
let result = preamble.join('\n');
|
|
56
|
+
for (const heading of order) {
|
|
57
|
+
const content = sections.get(heading);
|
|
58
|
+
// Trim trailing empty lines from section
|
|
59
|
+
while (content.length > 0 && content[content.length - 1].trim() === '')
|
|
60
|
+
content.pop();
|
|
61
|
+
result += '\n\n' + heading + '\n' + content.join('\n');
|
|
62
|
+
}
|
|
63
|
+
return result.replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
64
|
+
}
|
|
65
|
+
export function registerMemoryTools(server) {
|
|
66
|
+
// ── 0. working_memory ──────────────────────────────────────────────────
|
|
67
|
+
server.tool('working_memory', getToolDescription('working_memory') ?? '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.', {
|
|
68
|
+
action: z.enum(['read', 'append', 'replace', 'clear']).describe('What to do with working memory'),
|
|
69
|
+
content: z.string().optional().describe('Text to append or replace with (required for append/replace)'),
|
|
70
|
+
}, async ({ action, content }) => {
|
|
71
|
+
const wmFile = agentWorkingMemoryFile(ACTIVE_AGENT_SLUG);
|
|
72
|
+
switch (action) {
|
|
73
|
+
case 'read': {
|
|
74
|
+
if (!existsSync(wmFile)) {
|
|
75
|
+
return textResult('Working memory is empty.');
|
|
76
|
+
}
|
|
77
|
+
const text = readFileSync(wmFile, 'utf-8');
|
|
78
|
+
const lineCount = text.split('\n').length;
|
|
79
|
+
let result = text;
|
|
80
|
+
if (lineCount > WORKING_MEMORY_MAX_LINES) {
|
|
81
|
+
result += `\n\n⚠️ Working memory is ${lineCount} lines (limit: ${WORKING_MEMORY_MAX_LINES}). Consider compacting — remove resolved items and summarize.`;
|
|
82
|
+
}
|
|
83
|
+
return textResult(result);
|
|
84
|
+
}
|
|
85
|
+
case 'append': {
|
|
86
|
+
if (!content)
|
|
87
|
+
return textResult('Error: content is required for append.');
|
|
88
|
+
const existing = existsSync(wmFile) ? readFileSync(wmFile, 'utf-8') : '';
|
|
89
|
+
const separator = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
90
|
+
writeFileSync(wmFile, existing + separator + content + '\n');
|
|
91
|
+
const newLineCount = (existing + separator + content).split('\n').length;
|
|
92
|
+
let msg = `Appended to working memory.`;
|
|
93
|
+
if (newLineCount > WORKING_MEMORY_MAX_LINES) {
|
|
94
|
+
msg += ` ⚠️ Now ${newLineCount} lines — consider compacting.`;
|
|
95
|
+
}
|
|
96
|
+
return textResult(msg);
|
|
97
|
+
}
|
|
98
|
+
case 'replace': {
|
|
99
|
+
if (!content)
|
|
100
|
+
return textResult('Error: content is required for replace.');
|
|
101
|
+
writeFileSync(wmFile, content + '\n');
|
|
102
|
+
return textResult('Working memory replaced.');
|
|
103
|
+
}
|
|
104
|
+
case 'clear': {
|
|
105
|
+
if (existsSync(wmFile))
|
|
106
|
+
unlinkSync(wmFile);
|
|
107
|
+
return textResult('Working memory cleared.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// ── 1. memory_read ─────────────────────────────────────────────────────
|
|
112
|
+
server.tool('memory_read', getToolDescription('memory_read') ?? "Read a note from the Obsidian vault. Shortcuts: 'today', 'yesterday', 'memory', 'tasks', 'heartbeat', 'cron', 'soul'. Or pass a relative path or note name.", { name: z.string().describe('Note name, path, or shortcut') }, async ({ name }) => {
|
|
113
|
+
const filePath = resolvePath(name);
|
|
114
|
+
if (!existsSync(filePath)) {
|
|
115
|
+
return textResult(`Note not found: ${name}`);
|
|
116
|
+
}
|
|
117
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
118
|
+
const rel = path.relative(VAULT_DIR, filePath);
|
|
119
|
+
return textResult(`**${rel}:**\n\n${content}`);
|
|
120
|
+
});
|
|
121
|
+
// ── 2. memory_write ────────────────────────────────────────────────────
|
|
122
|
+
server.tool('memory_write', getToolDescription('memory_write') ?? "Write or append to a vault note. Actions: 'append_daily' (add to today's log), 'update_memory' (update MEMORY.md section), 'write_note' (write/overwrite a note), 'update_identity' (set identity seed — who you are, your role, key context).", {
|
|
123
|
+
action: z.enum(['append_daily', 'update_memory', 'write_note', 'update_identity']).describe('Write action'),
|
|
124
|
+
content: z.string().describe('Text to write/append'),
|
|
125
|
+
section: z.string().optional().describe('Section for append_daily or update_memory'),
|
|
126
|
+
file_path: z.string().optional().describe('Relative vault path for write_note action'),
|
|
127
|
+
}, async ({ action, content, section, file_path }) => {
|
|
128
|
+
if (action === 'append_daily') {
|
|
129
|
+
const sec = section ?? 'Interactions';
|
|
130
|
+
const dailyPath = ensureDailyNote();
|
|
131
|
+
let body = readFileSync(dailyPath, 'utf-8');
|
|
132
|
+
const timestamp = nowTime();
|
|
133
|
+
const entry = `\n- **${timestamp}** — ${content}`;
|
|
134
|
+
const pattern = new RegExp(`(## ${sec.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*?)(\\n## |$)`, 's');
|
|
135
|
+
const match = pattern.exec(body);
|
|
136
|
+
if (match) {
|
|
137
|
+
body = body.slice(0, match.index + match[1].length) + entry + body.slice(match.index + match[1].length);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
body += `\n\n## ${sec}${entry}`;
|
|
141
|
+
}
|
|
142
|
+
writeFileSync(dailyPath, body, 'utf-8');
|
|
143
|
+
const rel = path.relative(VAULT_DIR, dailyPath);
|
|
144
|
+
await incrementalSync(rel, ACTIVE_AGENT_SLUG ?? undefined);
|
|
145
|
+
return textResult(`Appended to ${path.basename(dailyPath)} > ${sec}`);
|
|
146
|
+
}
|
|
147
|
+
if (action === 'update_memory') {
|
|
148
|
+
const sec = section ?? '';
|
|
149
|
+
if (!sec)
|
|
150
|
+
return textResult("Error: 'section' required for update_memory");
|
|
151
|
+
// Resolve target MEMORY.md: agent-specific if running as team agent, else global
|
|
152
|
+
let targetMemFile = MEMORY_FILE;
|
|
153
|
+
if (ACTIVE_AGENT_SLUG) {
|
|
154
|
+
const agentMemDir = path.join(SYSTEM_DIR, 'agents', ACTIVE_AGENT_SLUG);
|
|
155
|
+
mkdirSync(agentMemDir, { recursive: true });
|
|
156
|
+
targetMemFile = path.join(agentMemDir, 'MEMORY.md');
|
|
157
|
+
if (!existsSync(targetMemFile)) {
|
|
158
|
+
writeFileSync(targetMemFile, `# ${ACTIVE_AGENT_SLUG} Memory\n\n`, 'utf-8');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Dedup check against indexed memory — bump salience instead of discarding
|
|
162
|
+
try {
|
|
163
|
+
const store = await getStore();
|
|
164
|
+
const dup = store.checkDuplicate(content, path.relative(VAULT_DIR, targetMemFile));
|
|
165
|
+
if (dup.isDuplicate && dup.matchId) {
|
|
166
|
+
// Reinforce the existing chunk — the fact was mentioned again, so it's important
|
|
167
|
+
store.bumpChunkSalience(dup.matchId, 0.1);
|
|
168
|
+
store.logExtraction({
|
|
169
|
+
sessionKey: 'mcp', userMessage: content.slice(0, 200),
|
|
170
|
+
toolName: 'memory_write', toolInput: JSON.stringify({ action, section: sec }),
|
|
171
|
+
extractedAt: new Date().toISOString(), status: 'dedup_skipped',
|
|
172
|
+
agentSlug: ACTIVE_AGENT_SLUG ?? undefined,
|
|
173
|
+
});
|
|
174
|
+
return textResult(`Reinforced existing memory (chunk #${dup.matchId}, salience bumped). No duplicate written.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch { /* dedup failure is non-fatal — proceed with write */ }
|
|
178
|
+
let body = readFileSync(targetMemFile, 'utf-8');
|
|
179
|
+
// First, merge any duplicate section headers in the file
|
|
180
|
+
body = mergeDuplicateSections(body);
|
|
181
|
+
const pattern = new RegExp(`(## ${sec.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n)(.*?)(\\n## |$)`, 's');
|
|
182
|
+
const match = pattern.exec(body);
|
|
183
|
+
if (match) {
|
|
184
|
+
const existingContent = match[2].trim();
|
|
185
|
+
const existingLines = existingContent.split('\n').map(l => l.trim()).filter(Boolean);
|
|
186
|
+
const newLines = content.split('\n').map(l => l.trim()).filter(Boolean);
|
|
187
|
+
// Dedup: skip lines that are exact or near-exact duplicates
|
|
188
|
+
const filtered = [];
|
|
189
|
+
for (const newLine of newLines) {
|
|
190
|
+
const isDup = existingLines.some(ex => {
|
|
191
|
+
const a = newLine.toLowerCase().trim();
|
|
192
|
+
const b = ex.toLowerCase().trim();
|
|
193
|
+
if (a === b)
|
|
194
|
+
return true;
|
|
195
|
+
// Fuzzy: if one line contains 80%+ of the other's words, treat as dup
|
|
196
|
+
const aWords = a.split(/\s+/);
|
|
197
|
+
const bWords = b.split(/\s+/);
|
|
198
|
+
if (aWords.length < 3 || bWords.length < 3)
|
|
199
|
+
return false;
|
|
200
|
+
const overlap = aWords.filter(w => bWords.includes(w)).length;
|
|
201
|
+
return overlap / Math.max(aWords.length, bWords.length) > 0.8;
|
|
202
|
+
});
|
|
203
|
+
if (!isDup) {
|
|
204
|
+
filtered.push(newLine);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!filtered.length) {
|
|
208
|
+
return textResult(`No new information for MEMORY.md > ${sec} (all duplicates)`);
|
|
209
|
+
}
|
|
210
|
+
const updatedText = existingContent + '\n' + filtered.join('\n');
|
|
211
|
+
body = body.slice(0, match.index + match[1].length) + updatedText + '\n' + body.slice(match.index + match[1].length + match[2].length);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
body += `\n\n## ${sec}\n\n${content}\n`;
|
|
215
|
+
}
|
|
216
|
+
writeFileSync(targetMemFile, body, 'utf-8');
|
|
217
|
+
const rel = path.relative(VAULT_DIR, targetMemFile);
|
|
218
|
+
await incrementalSync(rel, ACTIVE_AGENT_SLUG ?? undefined);
|
|
219
|
+
const label = ACTIVE_AGENT_SLUG ? `${ACTIVE_AGENT_SLUG}/MEMORY.md` : 'MEMORY.md';
|
|
220
|
+
return textResult(`Updated ${label} > ${sec}`);
|
|
221
|
+
}
|
|
222
|
+
if (action === 'update_identity') {
|
|
223
|
+
mkdirSync(path.dirname(IDENTITY_FILE), { recursive: true });
|
|
224
|
+
writeFileSync(IDENTITY_FILE, content, 'utf-8');
|
|
225
|
+
const rel = path.relative(VAULT_DIR, IDENTITY_FILE);
|
|
226
|
+
await incrementalSync(rel, ACTIVE_AGENT_SLUG ?? undefined);
|
|
227
|
+
return textResult('Updated identity seed (IDENTITY.md)');
|
|
228
|
+
}
|
|
229
|
+
if (action === 'write_note') {
|
|
230
|
+
const relPath = file_path ?? '';
|
|
231
|
+
if (!relPath)
|
|
232
|
+
return textResult("Error: 'file_path' required for write_note");
|
|
233
|
+
const full = validateVaultPath(relPath);
|
|
234
|
+
mkdirSync(path.dirname(full), { recursive: true });
|
|
235
|
+
writeFileSync(full, content, 'utf-8');
|
|
236
|
+
await incrementalSync(relPath, ACTIVE_AGENT_SLUG ?? undefined);
|
|
237
|
+
return textResult(`Wrote: ${relPath}`);
|
|
238
|
+
}
|
|
239
|
+
return textResult(`Unknown action: ${action}`);
|
|
240
|
+
});
|
|
241
|
+
// ── 3. memory_search ───────────────────────────────────────────────────
|
|
242
|
+
server.tool('memory_search', getToolDescription('memory_search') ?? 'FTS5 search across all vault notes. Returns matching chunks with relevance scores. Optional category/topic filters narrow results.', {
|
|
243
|
+
query: z.string().describe('Search text'),
|
|
244
|
+
limit: z.number().optional().describe('Max results (default 20)'),
|
|
245
|
+
category: z.enum(['facts', 'events', 'discoveries', 'preferences', 'advice']).optional().describe('Filter by category'),
|
|
246
|
+
topic: z.string().optional().describe('Filter by topic'),
|
|
247
|
+
}, async ({ query, limit, category, topic }) => {
|
|
248
|
+
const maxResults = limit ?? 20;
|
|
249
|
+
const filters = (category || topic) ? { category, topic } : undefined;
|
|
250
|
+
try {
|
|
251
|
+
const store = await getStore();
|
|
252
|
+
const results = store.searchFts(query, maxResults, filters);
|
|
253
|
+
// Apply agent affinity boost
|
|
254
|
+
if (ACTIVE_AGENT_SLUG && results.length > 0) {
|
|
255
|
+
for (const r of results) {
|
|
256
|
+
if (r.agentSlug === ACTIVE_AGENT_SLUG)
|
|
257
|
+
r.score *= 1.4;
|
|
258
|
+
}
|
|
259
|
+
results.sort((a, b) => b.score - a.score);
|
|
260
|
+
}
|
|
261
|
+
if (results.length > 0) {
|
|
262
|
+
const lines = results.map(r => {
|
|
263
|
+
const preview = r.content.slice(0, 200).replace(/\n/g, ' ');
|
|
264
|
+
return `**${r.sourceFile} > ${r.section}** (score: ${r.score.toFixed(2)}) — ${preview}`;
|
|
265
|
+
});
|
|
266
|
+
return textResult(lines.join('\n'));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
logger.warn({ err }, 'FTS5 search failed, falling back to linear scan');
|
|
271
|
+
}
|
|
272
|
+
// Fallback: linear scan
|
|
273
|
+
const qLower = query.toLowerCase();
|
|
274
|
+
const results = [];
|
|
275
|
+
const mdFiles = globMd(VAULT_DIR);
|
|
276
|
+
for (const filePath of mdFiles) {
|
|
277
|
+
try {
|
|
278
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
279
|
+
const lines = content.split('\n');
|
|
280
|
+
for (let i = 0; i < lines.length; i++) {
|
|
281
|
+
if (qLower && lines[i].toLowerCase().includes(qLower)) {
|
|
282
|
+
const rel = path.relative(VAULT_DIR, filePath);
|
|
283
|
+
results.push(`**${rel}:${i + 1}** — ${lines[i].trim()}`);
|
|
284
|
+
if (results.length >= maxResults)
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (results.length >= maxResults)
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
if (!results.length) {
|
|
296
|
+
return textResult(`No results for: ${query}`);
|
|
297
|
+
}
|
|
298
|
+
return textResult(results.join('\n'));
|
|
299
|
+
});
|
|
300
|
+
// ── 4. memory_recall ───────────────────────────────────────────────────
|
|
301
|
+
server.tool('memory_recall', getToolDescription('memory_recall') ?? 'Context retrieval combining FTS5 relevance + recency search. Better than memory_search for finding related content by meaning. Optional category/topic filters narrow results.', {
|
|
302
|
+
query: z.string().describe('Natural language search query'),
|
|
303
|
+
category: z.enum(['facts', 'events', 'discoveries', 'preferences', 'advice']).optional().describe('Filter by category'),
|
|
304
|
+
topic: z.string().optional().describe('Filter by topic'),
|
|
305
|
+
}, async ({ query, category, topic }) => {
|
|
306
|
+
const store = await getStore();
|
|
307
|
+
const results = store.searchContext(query, { agentSlug: ACTIVE_AGENT_SLUG ?? undefined, category, topic });
|
|
308
|
+
if (!results.length) {
|
|
309
|
+
return textResult(`No results for: ${query}`);
|
|
310
|
+
}
|
|
311
|
+
// Record access for salience tracking
|
|
312
|
+
const chunkIds = results.map(r => r.chunkId).filter(Boolean);
|
|
313
|
+
if (chunkIds.length)
|
|
314
|
+
store.recordAccess(chunkIds);
|
|
315
|
+
const lines = results.map(r => {
|
|
316
|
+
const label = `[${r.matchType}]`;
|
|
317
|
+
const preview = r.content.slice(0, 300).replace(/\n/g, ' ');
|
|
318
|
+
return `**${r.sourceFile} > ${r.section}** ${label} (score: ${r.score.toFixed(3)})\n${preview}\n`;
|
|
319
|
+
});
|
|
320
|
+
return textResult(lines.join('\n'));
|
|
321
|
+
});
|
|
322
|
+
// ── 10. memory_connections ─────────────────────────────────────────────
|
|
323
|
+
server.tool('memory_connections', 'Query the wikilink graph — find all notes connected to/from a given note.', {
|
|
324
|
+
note_name: z.string().describe('Note name (without .md) to find connections for'),
|
|
325
|
+
}, async ({ note_name }) => {
|
|
326
|
+
try {
|
|
327
|
+
const store = await getStore();
|
|
328
|
+
const connections = store.getConnections(note_name);
|
|
329
|
+
const outgoing = connections.filter(c => c.direction === 'outgoing');
|
|
330
|
+
const incoming = connections.filter(c => c.direction === 'incoming');
|
|
331
|
+
const lines = [`**Connections for [[${note_name}]]:**\n`];
|
|
332
|
+
if (outgoing.length) {
|
|
333
|
+
lines.push(`**Links to (${outgoing.length}):**`);
|
|
334
|
+
const seen = new Set();
|
|
335
|
+
for (const c of outgoing) {
|
|
336
|
+
if (!seen.has(c.file)) {
|
|
337
|
+
lines.push(` → [[${c.file}]] — _${c.context.slice(0, 100)}_`);
|
|
338
|
+
seen.add(c.file);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (incoming.length) {
|
|
343
|
+
lines.push(`\n**Linked from (${incoming.length}):**`);
|
|
344
|
+
const seen = new Set();
|
|
345
|
+
for (const c of incoming) {
|
|
346
|
+
if (!seen.has(c.file)) {
|
|
347
|
+
lines.push(` ← ${c.file} — _${c.context.slice(0, 100)}_`);
|
|
348
|
+
seen.add(c.file);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (!connections.length) {
|
|
353
|
+
return textResult(`No connections found for: ${note_name}`);
|
|
354
|
+
}
|
|
355
|
+
return textResult(lines.join('\n'));
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
return textResult(`Error querying connections: ${err}`);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
// ── 11. memory_timeline ───────────────────────────────────────────────
|
|
362
|
+
server.tool('memory_timeline', 'Chronological view of memory/vault changes within a date range. Great for "what happened last week" queries.', {
|
|
363
|
+
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
|
|
364
|
+
end_date: z.string().optional().describe('End date (YYYY-MM-DD, default: today)'),
|
|
365
|
+
limit: z.number().optional().describe('Max results (default 20)'),
|
|
366
|
+
}, async ({ start_date, end_date, limit }) => {
|
|
367
|
+
const endD = end_date ?? todayStr();
|
|
368
|
+
const maxResults = limit ?? 20;
|
|
369
|
+
try {
|
|
370
|
+
const store = await getStore();
|
|
371
|
+
const results = store.getTimeline(start_date, endD, maxResults);
|
|
372
|
+
if (!results.length) {
|
|
373
|
+
return textResult(`No activity between ${start_date} and ${endD}`);
|
|
374
|
+
}
|
|
375
|
+
const lines = [`**Timeline: ${start_date} → ${endD}** (${results.length} items)\n`];
|
|
376
|
+
for (const r of results) {
|
|
377
|
+
const date = r.lastUpdated?.slice(0, 16).replace('T', ' ') ?? '?';
|
|
378
|
+
const preview = r.content.slice(0, 200).replace(/\n/g, ' ');
|
|
379
|
+
lines.push(`- **${date}** — ${r.sourceFile} > ${r.section}\n ${preview}`);
|
|
380
|
+
}
|
|
381
|
+
return textResult(lines.join('\n'));
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
return textResult(`Timeline error: ${err}`);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
// ── 12. transcript_search ─────────────────────────────────────────────
|
|
388
|
+
server.tool('transcript_search', 'Search past conversation transcripts by keyword. Returns matching turns with session context.', {
|
|
389
|
+
query: z.string().describe('Search text'),
|
|
390
|
+
limit: z.number().optional().describe('Max results (default 20)'),
|
|
391
|
+
session_key: z.string().optional().describe('Filter to a specific session'),
|
|
392
|
+
}, async ({ query, limit, session_key }) => {
|
|
393
|
+
const maxResults = limit ?? 20;
|
|
394
|
+
try {
|
|
395
|
+
const store = await getStore();
|
|
396
|
+
const results = store.searchTranscripts(query, maxResults, session_key ?? '');
|
|
397
|
+
if (!results.length) {
|
|
398
|
+
return textResult(`No transcript matches for: ${query}`);
|
|
399
|
+
}
|
|
400
|
+
const lines = [`**Transcript search: "${query}"** (${results.length} matches)\n`];
|
|
401
|
+
for (const r of results) {
|
|
402
|
+
const date = r.createdAt?.slice(0, 16).replace('T', ' ') ?? '?';
|
|
403
|
+
lines.push(`- **[${r.role}]** ${date} (session: ${r.sessionKey.slice(0, 8)}...)\n ${r.content}`);
|
|
404
|
+
}
|
|
405
|
+
return textResult(lines.join('\n'));
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
return textResult(`Transcript search error: ${err}`);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
// ── Memory Transparency: memory_report ──────────────────────────────────
|
|
412
|
+
server.tool('memory_report', 'Show recent memory extractions — what was learned, when, and from which message. Helps the owner verify what the assistant has been learning.', {
|
|
413
|
+
limit: z.number().optional().default(10).describe('Number of recent extractions to show'),
|
|
414
|
+
status: z.enum(['active', 'corrected', 'dismissed', 'all']).optional().default('all').describe('Filter by status'),
|
|
415
|
+
}, async ({ limit, status }) => {
|
|
416
|
+
const store = await getStore();
|
|
417
|
+
const filter = status === 'all' ? undefined : status;
|
|
418
|
+
const extractions = store.getRecentExtractions(limit, filter);
|
|
419
|
+
if (extractions.length === 0) {
|
|
420
|
+
return textResult('No memory extractions found.');
|
|
421
|
+
}
|
|
422
|
+
const report = extractions.map((e, i) => `${i + 1}. [${e.status}] ${e.extractedAt}\n From: "${e.userMessage.slice(0, 100)}${e.userMessage.length > 100 ? '...' : ''}"\n Tool: ${e.toolName}\n Input: ${e.toolInput.slice(0, 200)}${e.correction ? `\n Correction: ${e.correction}` : ''}`).join('\n\n');
|
|
423
|
+
return textResult(report);
|
|
424
|
+
});
|
|
425
|
+
// ── Memory Transparency: memory_correct ─────────────────────────────────
|
|
426
|
+
server.tool('memory_correct', 'Correct or dismiss a memory extraction. Use when the owner says something learned was wrong.', {
|
|
427
|
+
id: z.number().describe('Extraction ID from memory_report'),
|
|
428
|
+
action: z.enum(['correct', 'dismiss']).describe('Whether to correct (replace with accurate fact) or dismiss (mark as invalid)'),
|
|
429
|
+
correction: z.string().optional().describe('The corrected fact (required if action is "correct")'),
|
|
430
|
+
}, async ({ id, action, correction }) => {
|
|
431
|
+
const store = await getStore();
|
|
432
|
+
if (action === 'correct') {
|
|
433
|
+
if (!correction)
|
|
434
|
+
return textResult('Correction text required when action is "correct".');
|
|
435
|
+
// correctExtraction now also removes the wrong content from the search index
|
|
436
|
+
store.correctExtraction(id, correction);
|
|
437
|
+
return textResult(`Extraction #${id} corrected and removed from search index. Corrected fact: ${correction}`);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
// dismissExtraction now also removes the content from the search index
|
|
441
|
+
store.dismissExtraction(id);
|
|
442
|
+
return textResult(`Extraction #${id} dismissed and removed from search index.`);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// ── Memory Consolidation: memory_consolidate ────────────────────────────
|
|
446
|
+
server.tool('memory_consolidate', 'Get memory chunks that are candidates for consolidation, or mark chunks as consolidated after synthesis. Use this in weekly memory maintenance.', {
|
|
447
|
+
action: z.enum(['candidates', 'mark_consolidated']).describe('"candidates" returns groups of old chunks to consolidate. "mark_consolidated" marks chunks as archived after you\'ve written a summary.'),
|
|
448
|
+
min_age_days: z.number().optional().describe('Minimum age in days for consolidation candidates (default: 30)'),
|
|
449
|
+
chunk_ids: z.array(z.number()).optional().describe('Chunk IDs to mark as consolidated (required for mark_consolidated)'),
|
|
450
|
+
}, async ({ action, min_age_days, chunk_ids }) => {
|
|
451
|
+
const store = await getStore();
|
|
452
|
+
if (action === 'candidates') {
|
|
453
|
+
const groups = store.getConsolidationCandidates(min_age_days ?? 30);
|
|
454
|
+
if (groups.length === 0) {
|
|
455
|
+
const stats = store.getConsolidationStats();
|
|
456
|
+
return textResult(`No consolidation candidates found (${stats.totalChunks} total chunks, ${stats.consolidated} already consolidated).`);
|
|
457
|
+
}
|
|
458
|
+
const report = groups.slice(0, 10).map((g) => {
|
|
459
|
+
const preview = g.contents.slice(0, 3).map(c => ` - ${c.slice(0, 120)}`).join('\n');
|
|
460
|
+
return `**${g.topic}** (${g.chunkIds.length} chunks, ${g.totalChars} chars)\n${preview}${g.contents.length > 3 ? `\n ... and ${g.contents.length - 3} more` : ''}`;
|
|
461
|
+
}).join('\n\n');
|
|
462
|
+
const stats = store.getConsolidationStats();
|
|
463
|
+
return textResult(`Found ${groups.length} topic group(s) ready for consolidation.\n` +
|
|
464
|
+
`Stats: ${stats.totalChunks} total, ${stats.consolidated} consolidated, ${stats.unconsolidated} unconsolidated.\n\n` +
|
|
465
|
+
`${report}\n\n` +
|
|
466
|
+
`To consolidate: read the chunks, write a summary note, then call memory_consolidate(action="mark_consolidated", chunk_ids=[...]) with the original chunk IDs.`);
|
|
467
|
+
}
|
|
468
|
+
if (action === 'mark_consolidated') {
|
|
469
|
+
if (!chunk_ids || chunk_ids.length === 0) {
|
|
470
|
+
return textResult('Error: chunk_ids required for mark_consolidated action.');
|
|
471
|
+
}
|
|
472
|
+
store.markConsolidated(chunk_ids);
|
|
473
|
+
return textResult(`Marked ${chunk_ids.length} chunk(s) as consolidated (salience reduced).`);
|
|
474
|
+
}
|
|
475
|
+
return textResult('Unknown action.');
|
|
476
|
+
});
|
|
477
|
+
// ── Graph Memory Tools ─────────────────────────────────────────────────
|
|
478
|
+
const GRAPH_DB_DIR = path.join(BASE_DIR, '.graph.db');
|
|
479
|
+
let _graphStore = null;
|
|
480
|
+
async function getGraphStore() {
|
|
481
|
+
if (_graphStore?.isAvailable())
|
|
482
|
+
return _graphStore;
|
|
483
|
+
try {
|
|
484
|
+
const { getSharedGraphStore } = await import('../memory/graph-store.js');
|
|
485
|
+
_graphStore = await getSharedGraphStore(GRAPH_DB_DIR);
|
|
486
|
+
return _graphStore;
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
server.tool('memory_graph_query', 'Run a Cypher query against the knowledge graph. Returns entities and relationships. Use for complex graph traversals.', {
|
|
493
|
+
query: z.string().describe('Cypher query (e.g., MATCH (p:Person)-[:WORKS_ON]->(proj:Project) RETURN p.id, proj.id)'),
|
|
494
|
+
}, async ({ query }) => {
|
|
495
|
+
const gs = await getGraphStore();
|
|
496
|
+
if (!gs?.isAvailable()) {
|
|
497
|
+
return textResult('Graph features are not available. The knowledge graph has not been initialized.');
|
|
498
|
+
}
|
|
499
|
+
const results = await gs.query(query);
|
|
500
|
+
if (results.length === 0)
|
|
501
|
+
return textResult('No results.');
|
|
502
|
+
const formatted = results.map((row) => typeof row === 'object' ? Object.values(row).map(v => typeof v === 'object' ? JSON.stringify(v) : String(v)).join(' | ') : String(row)).join('\n');
|
|
503
|
+
return textResult(formatted);
|
|
504
|
+
});
|
|
505
|
+
server.tool('memory_graph_connections', 'Find entities connected to a given entity in the knowledge graph. Supports multi-hop traversal with typed relationships. Use as_of for point-in-time queries.', {
|
|
506
|
+
entity: z.string().describe('Entity ID (slug) to find connections for'),
|
|
507
|
+
max_hops: z.number().optional().describe('Maximum traversal depth (default: 2)'),
|
|
508
|
+
relationship_types: z.array(z.string()).optional().describe('Filter by relationship types (e.g., ["WORKS_ON", "KNOWS"])'),
|
|
509
|
+
as_of: z.string().optional().describe('ISO timestamp for point-in-time query (e.g., "2026-01-15T00:00:00Z"). Only shows relationships active at that time.'),
|
|
510
|
+
}, async ({ entity, max_hops, relationship_types, as_of }) => {
|
|
511
|
+
const gs = await getGraphStore();
|
|
512
|
+
if (!gs?.isAvailable()) {
|
|
513
|
+
return textResult('Graph features are not available. The knowledge graph has not been initialized.');
|
|
514
|
+
}
|
|
515
|
+
const results = await gs.traverse(entity, max_hops ?? 2, relationship_types, as_of);
|
|
516
|
+
if (results.length === 0)
|
|
517
|
+
return textResult(`No connections found for '${entity}'.`);
|
|
518
|
+
const lines = results.map((r) => `[depth ${r.depth}] ${r.entity.label}:${r.entity.id} (via ${r.path.join(' → ')})`);
|
|
519
|
+
return textResult(`Connections for '${entity}':\n${lines.join('\n')}`);
|
|
520
|
+
});
|
|
521
|
+
server.tool('memory_graph_path', 'Find the shortest path between two entities in the knowledge graph. Shows how they are connected.', {
|
|
522
|
+
from: z.string().describe('Source entity ID (slug)'),
|
|
523
|
+
to: z.string().describe('Target entity ID (slug)'),
|
|
524
|
+
}, async ({ from, to }) => {
|
|
525
|
+
const gs = await getGraphStore();
|
|
526
|
+
if (!gs?.isAvailable()) {
|
|
527
|
+
return textResult('Graph features are not available. The knowledge graph has not been initialized.');
|
|
528
|
+
}
|
|
529
|
+
const result = await gs.shortestPath(from, to);
|
|
530
|
+
if (!result)
|
|
531
|
+
return textResult(`No path found between '${from}' and '${to}'.`);
|
|
532
|
+
const chain = result.nodes.map((n, i) => {
|
|
533
|
+
const rel = result.relationships[i];
|
|
534
|
+
return rel ? `${n.id} -[${rel}]->` : n.id;
|
|
535
|
+
}).join(' ');
|
|
536
|
+
return textResult(`Path (${result.length} hops): ${chain}`);
|
|
537
|
+
});
|
|
538
|
+
server.tool('promote_memory_to_global', 'Promote one of your private memories to global shared memory, making it visible to all agents. ' +
|
|
539
|
+
'Use when you have learned something universally useful that other agents should also know. ' +
|
|
540
|
+
'Requires a chunk ID (from memory_search results). Owner is notified of all promotions.', {
|
|
541
|
+
chunk_id: z.number().describe('ID of the memory chunk to promote (from memory_search results)'),
|
|
542
|
+
reason: z.string().describe('Why this insight is worth sharing globally — shown in the audit log'),
|
|
543
|
+
}, async ({ chunk_id, reason }) => {
|
|
544
|
+
const store = await getStore();
|
|
545
|
+
if (!store)
|
|
546
|
+
return textResult('Memory store not available.');
|
|
547
|
+
const result = store.promoteToGlobal(chunk_id, ACTIVE_AGENT_SLUG ?? 'unknown');
|
|
548
|
+
logger.info({ chunkId: chunk_id, promotedBy: ACTIVE_AGENT_SLUG, reason }, 'Memory promoted to global');
|
|
549
|
+
return textResult(`${result}\n\nReason: ${reason}`);
|
|
550
|
+
});
|
|
551
|
+
server.tool('memory_graph_invalidate', 'Mark a relationship as no longer active by setting its end date. Use when a fact changes (e.g., someone leaves a company).', {
|
|
552
|
+
from_entity: z.string().describe('Source entity ID (slug)'),
|
|
553
|
+
to_entity: z.string().describe('Target entity ID (slug)'),
|
|
554
|
+
relationship_type: z.string().describe('Relationship type (e.g., WORKS_AT)'),
|
|
555
|
+
as_of: z.string().optional().describe('ISO timestamp when relationship ended (defaults to now)'),
|
|
556
|
+
}, async ({ from_entity, to_entity, relationship_type, as_of }) => {
|
|
557
|
+
const gs = await getGraphStore();
|
|
558
|
+
if (!gs?.isAvailable()) {
|
|
559
|
+
return textResult('Graph features are not available. The knowledge graph has not been initialized.');
|
|
560
|
+
}
|
|
561
|
+
const updated = await gs.invalidateRelationship(from_entity, to_entity, relationship_type, as_of);
|
|
562
|
+
if (updated) {
|
|
563
|
+
return textResult(`Invalidated ${from_entity} -[${relationship_type}]-> ${to_entity} (ended ${as_of ?? 'now'})`);
|
|
564
|
+
}
|
|
565
|
+
return textResult(`No active ${relationship_type} relationship found between '${from_entity}' and '${to_entity}'.`);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
//# sourceMappingURL=memory-tools.js.map
|