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,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Vault file chunker for memory indexing.
|
|
3
|
+
*
|
|
4
|
+
* Parses Markdown files into chunks by ## headers, extracts frontmatter,
|
|
5
|
+
* and splits oversized sections at paragraph boundaries.
|
|
6
|
+
*/
|
|
7
|
+
import type { Chunk } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Parse a Markdown file into chunks by ## headers.
|
|
10
|
+
*
|
|
11
|
+
* @param filePath - Absolute path to the Markdown file.
|
|
12
|
+
* @param vaultDir - Absolute path to the vault root.
|
|
13
|
+
* @returns List of Chunk objects. Empty if file should be skipped.
|
|
14
|
+
*/
|
|
15
|
+
export declare function chunkFile(filePath: string, vaultDir: string): Chunk[];
|
|
16
|
+
/**
|
|
17
|
+
* Split Markdown body by ## headers.
|
|
18
|
+
*
|
|
19
|
+
* Content before the first ## header is labeled "preamble".
|
|
20
|
+
*
|
|
21
|
+
* @returns Array of [sectionName, sectionContent] tuples.
|
|
22
|
+
*/
|
|
23
|
+
export declare function splitByHeaders(body: string): [string, string][];
|
|
24
|
+
/**
|
|
25
|
+
* Split text at paragraph boundaries (double newlines) to stay under maxChars.
|
|
26
|
+
*/
|
|
27
|
+
export declare function splitAtParagraphs(text: string, maxChars: number): string[];
|
|
28
|
+
//# sourceMappingURL=chunker.d.ts.map
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Vault file chunker for memory indexing.
|
|
3
|
+
*
|
|
4
|
+
* Parses Markdown files into chunks by ## headers, extracts frontmatter,
|
|
5
|
+
* and splits oversized sections at paragraph boundaries.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import matter from 'gray-matter';
|
|
11
|
+
/** Directories to skip when scanning the vault. */
|
|
12
|
+
const SKIP_DIRS = new Set(['06-Templates', '.obsidian']);
|
|
13
|
+
/** Maximum chunk size before splitting at paragraph boundaries. */
|
|
14
|
+
const MAX_CHUNK_CHARS = 3000;
|
|
15
|
+
/** Directory-to-category mapping for vault structure. */
|
|
16
|
+
const DIR_CATEGORY_MAP = {
|
|
17
|
+
'00-System': 'advice',
|
|
18
|
+
'01-Daily-Notes': 'events',
|
|
19
|
+
'02-People': 'facts',
|
|
20
|
+
'03-Projects': 'discoveries',
|
|
21
|
+
'04-Topics': 'facts',
|
|
22
|
+
'05-Tasks': 'advice',
|
|
23
|
+
'07-Inbox': 'events',
|
|
24
|
+
};
|
|
25
|
+
/** Content keyword patterns for category detection (used as fallback). */
|
|
26
|
+
const CATEGORY_KEYWORDS = [
|
|
27
|
+
[/\b(prefer|always use|never use|i like|i don'?t like|i hate)\b/i, 'preferences'],
|
|
28
|
+
[/\b(learned|discovered|TIL|turns out|insight|breakthrough)\b/i, 'discoveries'],
|
|
29
|
+
[/\b(reminder|tip|rule of thumb|always|never|best practice)\b/i, 'advice'],
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Detect category and topic for a chunk based on vault path, frontmatter, and content.
|
|
33
|
+
*/
|
|
34
|
+
function detectCategoryAndTopic(relPath, frontmatter, content) {
|
|
35
|
+
// Category detection (cascade)
|
|
36
|
+
let category = null;
|
|
37
|
+
// 1. Explicit frontmatter category
|
|
38
|
+
if (frontmatter.category) {
|
|
39
|
+
const fm = String(frontmatter.category).toLowerCase();
|
|
40
|
+
if (['facts', 'events', 'discoveries', 'preferences', 'advice'].includes(fm)) {
|
|
41
|
+
category = fm;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// 2. Directory-based
|
|
45
|
+
if (!category) {
|
|
46
|
+
const topDir = relPath.split('/')[0];
|
|
47
|
+
category = DIR_CATEGORY_MAP[topDir] ?? null;
|
|
48
|
+
}
|
|
49
|
+
// 3. Content keyword heuristics (only if nothing else matched)
|
|
50
|
+
if (!category) {
|
|
51
|
+
for (const [pattern, cat] of CATEGORY_KEYWORDS) {
|
|
52
|
+
if (pattern.test(content)) {
|
|
53
|
+
category = cat;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Topic detection (cascade)
|
|
59
|
+
let topic = null;
|
|
60
|
+
// 1. Explicit frontmatter topic or first tag
|
|
61
|
+
if (frontmatter.topic) {
|
|
62
|
+
topic = String(frontmatter.topic);
|
|
63
|
+
}
|
|
64
|
+
else if (Array.isArray(frontmatter.tags) && frontmatter.tags.length > 0) {
|
|
65
|
+
topic = String(frontmatter.tags[0]);
|
|
66
|
+
}
|
|
67
|
+
// 2. Second path segment (subdirectory name)
|
|
68
|
+
if (!topic) {
|
|
69
|
+
const parts = relPath.split('/');
|
|
70
|
+
if (parts.length >= 3) {
|
|
71
|
+
topic = parts[1];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { category, topic };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Compute a truncated SHA-256 content hash (first 16 hex chars).
|
|
78
|
+
*/
|
|
79
|
+
function contentHash(text) {
|
|
80
|
+
return createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parse a Markdown file into chunks by ## headers.
|
|
84
|
+
*
|
|
85
|
+
* @param filePath - Absolute path to the Markdown file.
|
|
86
|
+
* @param vaultDir - Absolute path to the vault root.
|
|
87
|
+
* @returns List of Chunk objects. Empty if file should be skipped.
|
|
88
|
+
*/
|
|
89
|
+
export function chunkFile(filePath, vaultDir) {
|
|
90
|
+
const relPath = path.relative(vaultDir, filePath);
|
|
91
|
+
// Skip templates and .obsidian
|
|
92
|
+
for (const skip of SKIP_DIRS) {
|
|
93
|
+
if (relPath.startsWith(skip)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
let raw;
|
|
98
|
+
try {
|
|
99
|
+
raw = readFileSync(filePath, 'utf-8');
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = matter(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const fmJson = parsed.data && Object.keys(parsed.data).length > 0
|
|
112
|
+
? JSON.stringify(parsed.data)
|
|
113
|
+
: '';
|
|
114
|
+
const chunks = [];
|
|
115
|
+
const { category, topic } = detectCategoryAndTopic(relPath, parsed.data ?? {}, parsed.content);
|
|
116
|
+
// Add frontmatter as its own chunk if present
|
|
117
|
+
if (parsed.data && Object.keys(parsed.data).length > 0) {
|
|
118
|
+
const fmText = Object.entries(parsed.data)
|
|
119
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
120
|
+
.join('\n');
|
|
121
|
+
chunks.push({
|
|
122
|
+
sourceFile: relPath,
|
|
123
|
+
section: 'frontmatter',
|
|
124
|
+
content: fmText,
|
|
125
|
+
chunkType: 'frontmatter',
|
|
126
|
+
frontmatterJson: fmJson,
|
|
127
|
+
contentHash: contentHash(fmText),
|
|
128
|
+
category,
|
|
129
|
+
topic,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// Split body by ## headers
|
|
133
|
+
const sections = splitByHeaders(parsed.content);
|
|
134
|
+
for (const [sectionName, sectionContent] of sections) {
|
|
135
|
+
const content = sectionContent.trim();
|
|
136
|
+
if (!content)
|
|
137
|
+
continue;
|
|
138
|
+
const chunkType = sectionName === 'preamble' ? 'preamble' : 'heading';
|
|
139
|
+
// Split oversized sections at paragraph boundaries
|
|
140
|
+
if (content.length > MAX_CHUNK_CHARS) {
|
|
141
|
+
const subChunks = splitAtParagraphs(content, MAX_CHUNK_CHARS);
|
|
142
|
+
for (let i = 0; i < subChunks.length; i++) {
|
|
143
|
+
const label = subChunks.length > 1 ? `${sectionName} (part ${i + 1})` : sectionName;
|
|
144
|
+
chunks.push({
|
|
145
|
+
sourceFile: relPath,
|
|
146
|
+
section: label,
|
|
147
|
+
chunkType,
|
|
148
|
+
content: subChunks[i],
|
|
149
|
+
frontmatterJson: fmJson,
|
|
150
|
+
contentHash: contentHash(subChunks[i]),
|
|
151
|
+
category,
|
|
152
|
+
topic,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
chunks.push({
|
|
158
|
+
sourceFile: relPath,
|
|
159
|
+
section: sectionName,
|
|
160
|
+
chunkType,
|
|
161
|
+
content,
|
|
162
|
+
frontmatterJson: fmJson,
|
|
163
|
+
contentHash: contentHash(content),
|
|
164
|
+
category,
|
|
165
|
+
topic,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return chunks;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Split Markdown body by ## headers.
|
|
173
|
+
*
|
|
174
|
+
* Content before the first ## header is labeled "preamble".
|
|
175
|
+
*
|
|
176
|
+
* @returns Array of [sectionName, sectionContent] tuples.
|
|
177
|
+
*/
|
|
178
|
+
export function splitByHeaders(body) {
|
|
179
|
+
const sections = [];
|
|
180
|
+
let currentName = 'preamble';
|
|
181
|
+
let currentLines = [];
|
|
182
|
+
for (const line of body.split('\n')) {
|
|
183
|
+
if (line.startsWith('## ')) {
|
|
184
|
+
// Save previous section
|
|
185
|
+
if (currentLines.length > 0) {
|
|
186
|
+
sections.push([currentName, currentLines.join('\n')]);
|
|
187
|
+
}
|
|
188
|
+
currentName = line.slice(3).trim();
|
|
189
|
+
currentLines = [];
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
currentLines.push(line);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Don't forget the last section
|
|
196
|
+
if (currentLines.length > 0) {
|
|
197
|
+
sections.push([currentName, currentLines.join('\n')]);
|
|
198
|
+
}
|
|
199
|
+
return sections;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Split text at paragraph boundaries (double newlines) to stay under maxChars.
|
|
203
|
+
*/
|
|
204
|
+
export function splitAtParagraphs(text, maxChars) {
|
|
205
|
+
const paragraphs = text.split('\n\n');
|
|
206
|
+
const chunks = [];
|
|
207
|
+
let current = [];
|
|
208
|
+
let currentLen = 0;
|
|
209
|
+
for (const para of paragraphs) {
|
|
210
|
+
const paraLen = para.length + 2; // +2 for the \n\n separator
|
|
211
|
+
if (currentLen + paraLen > maxChars && current.length > 0) {
|
|
212
|
+
chunks.push(current.join('\n\n'));
|
|
213
|
+
current = [para];
|
|
214
|
+
currentLen = para.length;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
current.push(para);
|
|
218
|
+
currentLen += paraLen;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (current.length > 0) {
|
|
222
|
+
chunks.push(current.join('\n\n'));
|
|
223
|
+
}
|
|
224
|
+
return chunks;
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=chunker.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Memory Consolidation Engine.
|
|
3
|
+
*
|
|
4
|
+
* Three strategies that run during the evening consolidation window:
|
|
5
|
+
* 1. Fact Dedup — merge chunks with high Jaccard similarity (pure SQL + local compute)
|
|
6
|
+
* 2. Topic Summarization — LLM (Haiku) summarizes groups of 5+ chunks into a single summary
|
|
7
|
+
* 3. Principle Extraction — distill repeated behavioral corrections into permanent rules
|
|
8
|
+
*
|
|
9
|
+
* Source chunks are marked consolidated (never deleted) — they remain searchable at lower salience.
|
|
10
|
+
*/
|
|
11
|
+
export interface ConsolidationResult {
|
|
12
|
+
deduped: number;
|
|
13
|
+
summarized: number;
|
|
14
|
+
principlesExtracted: number;
|
|
15
|
+
errors: string[];
|
|
16
|
+
}
|
|
17
|
+
interface MemoryStoreHandle {
|
|
18
|
+
getConsolidationCandidates(minAgeDays: number): Array<{
|
|
19
|
+
topic: string;
|
|
20
|
+
chunkIds: number[];
|
|
21
|
+
contents: string[];
|
|
22
|
+
totalChars: number;
|
|
23
|
+
}>;
|
|
24
|
+
markConsolidated(chunkIds: number[]): void;
|
|
25
|
+
getConsolidationStats(): {
|
|
26
|
+
totalChunks: number;
|
|
27
|
+
consolidated: number;
|
|
28
|
+
unconsolidated: number;
|
|
29
|
+
};
|
|
30
|
+
insertSummaryChunk(sourceFile: string, section: string, content: string): void;
|
|
31
|
+
getBehavioralPatterns(minOccurrences: number): Array<{
|
|
32
|
+
correction: string;
|
|
33
|
+
count: number;
|
|
34
|
+
category: string;
|
|
35
|
+
lastSeen: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run all consolidation strategies in sequence.
|
|
40
|
+
* Budget-aware: stops if token/cost estimate exceeds limits.
|
|
41
|
+
*/
|
|
42
|
+
export declare function runConsolidation(store: MemoryStoreHandle, llmCall?: (prompt: string) => Promise<string>): Promise<ConsolidationResult>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=consolidation.d.ts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Memory Consolidation Engine.
|
|
3
|
+
*
|
|
4
|
+
* Three strategies that run during the evening consolidation window:
|
|
5
|
+
* 1. Fact Dedup — merge chunks with high Jaccard similarity (pure SQL + local compute)
|
|
6
|
+
* 2. Topic Summarization — LLM (Haiku) summarizes groups of 5+ chunks into a single summary
|
|
7
|
+
* 3. Principle Extraction — distill repeated behavioral corrections into permanent rules
|
|
8
|
+
*
|
|
9
|
+
* Source chunks are marked consolidated (never deleted) — they remain searchable at lower salience.
|
|
10
|
+
*/
|
|
11
|
+
import pino from 'pino';
|
|
12
|
+
import { tokenize, jaccard } from './mmr.js';
|
|
13
|
+
const logger = pino({ name: 'clementine.consolidation' });
|
|
14
|
+
/**
|
|
15
|
+
* Run all consolidation strategies in sequence.
|
|
16
|
+
* Budget-aware: stops if token/cost estimate exceeds limits.
|
|
17
|
+
*/
|
|
18
|
+
export async function runConsolidation(store, llmCall) {
|
|
19
|
+
const result = {
|
|
20
|
+
deduped: 0,
|
|
21
|
+
summarized: 0,
|
|
22
|
+
principlesExtracted: 0,
|
|
23
|
+
errors: [],
|
|
24
|
+
};
|
|
25
|
+
// Strategy 1: Fact Deduplication (no LLM needed)
|
|
26
|
+
try {
|
|
27
|
+
result.deduped = deduplicateChunks(store);
|
|
28
|
+
logger.info({ deduped: result.deduped }, 'Fact deduplication complete');
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const msg = `Dedup failed: ${err}`;
|
|
32
|
+
result.errors.push(msg);
|
|
33
|
+
logger.warn({ err }, msg);
|
|
34
|
+
}
|
|
35
|
+
// Strategy 2: Topic Summarization (requires LLM)
|
|
36
|
+
if (llmCall) {
|
|
37
|
+
try {
|
|
38
|
+
result.summarized = await summarizeTopics(store, llmCall);
|
|
39
|
+
logger.info({ summarized: result.summarized }, 'Topic summarization complete');
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const msg = `Summarization failed: ${err}`;
|
|
43
|
+
result.errors.push(msg);
|
|
44
|
+
logger.warn({ err }, msg);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Strategy 3: Principle Extraction (requires LLM)
|
|
48
|
+
if (llmCall) {
|
|
49
|
+
try {
|
|
50
|
+
result.principlesExtracted = await extractPrinciples(store, llmCall);
|
|
51
|
+
logger.info({ principlesExtracted: result.principlesExtracted }, 'Principle extraction complete');
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
const msg = `Principle extraction failed: ${err}`;
|
|
55
|
+
result.errors.push(msg);
|
|
56
|
+
logger.warn({ err }, msg);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const stats = store.getConsolidationStats();
|
|
60
|
+
logger.info({
|
|
61
|
+
...result,
|
|
62
|
+
totalChunks: stats.totalChunks,
|
|
63
|
+
consolidated: stats.consolidated,
|
|
64
|
+
unconsolidated: stats.unconsolidated,
|
|
65
|
+
}, 'Consolidation cycle complete');
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Strategy 1: Merge chunks with >0.7 Jaccard similarity within the same topic.
|
|
70
|
+
* Pure local computation — no LLM call needed.
|
|
71
|
+
* Marks the lower-salience duplicate as consolidated.
|
|
72
|
+
*/
|
|
73
|
+
function deduplicateChunks(store) {
|
|
74
|
+
const candidates = store.getConsolidationCandidates(7); // chunks older than 7 days
|
|
75
|
+
let deduped = 0;
|
|
76
|
+
for (const group of candidates) {
|
|
77
|
+
if (group.chunkIds.length < 2)
|
|
78
|
+
continue;
|
|
79
|
+
// Tokenize all chunks in this topic group
|
|
80
|
+
const tokenSets = group.contents.map(c => tokenize(c));
|
|
81
|
+
const toConsolidate = [];
|
|
82
|
+
// Compare each pair — mark the shorter one as consolidated
|
|
83
|
+
for (let i = 0; i < tokenSets.length; i++) {
|
|
84
|
+
if (toConsolidate.includes(group.chunkIds[i]))
|
|
85
|
+
continue;
|
|
86
|
+
for (let j = i + 1; j < tokenSets.length; j++) {
|
|
87
|
+
if (toConsolidate.includes(group.chunkIds[j]))
|
|
88
|
+
continue;
|
|
89
|
+
const sim = jaccard(tokenSets[i], tokenSets[j]);
|
|
90
|
+
if (sim > 0.7) {
|
|
91
|
+
// Mark the shorter content as duplicate
|
|
92
|
+
const shorter = group.contents[i].length <= group.contents[j].length
|
|
93
|
+
? group.chunkIds[i]
|
|
94
|
+
: group.chunkIds[j];
|
|
95
|
+
toConsolidate.push(shorter);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (toConsolidate.length > 0) {
|
|
100
|
+
store.markConsolidated(toConsolidate);
|
|
101
|
+
deduped += toConsolidate.length;
|
|
102
|
+
}
|
|
103
|
+
// Cap per cycle to avoid long-running operations
|
|
104
|
+
if (deduped >= 50)
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
return deduped;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Strategy 2: For topic groups with 5+ unconsolidated chunks,
|
|
111
|
+
* generate a summary via Haiku and insert as a new summary chunk.
|
|
112
|
+
* Mark source chunks as consolidated.
|
|
113
|
+
*/
|
|
114
|
+
async function summarizeTopics(store, llmCall) {
|
|
115
|
+
const candidates = store.getConsolidationCandidates(14); // chunks older than 14 days
|
|
116
|
+
let summarized = 0;
|
|
117
|
+
// Process top 3 topic groups per cycle
|
|
118
|
+
for (const group of candidates.slice(0, 3)) {
|
|
119
|
+
if (group.chunkIds.length < 5)
|
|
120
|
+
continue;
|
|
121
|
+
// Build prompt for summarization
|
|
122
|
+
const contentSample = group.contents.slice(0, 10).join('\n---\n').slice(0, 3000);
|
|
123
|
+
const prompt = `Summarize the following ${group.chunkIds.length} memory chunks about "${group.topic}" ` +
|
|
124
|
+
`into 3-5 concise sentences that capture the essential facts. ` +
|
|
125
|
+
`Preserve specific names, dates, decisions, and identifiers. ` +
|
|
126
|
+
`Drop redundant or trivial information.\n\n` +
|
|
127
|
+
`Chunks:\n${contentSample}\n\n` +
|
|
128
|
+
`Output only the summary text, nothing else.`;
|
|
129
|
+
try {
|
|
130
|
+
const summary = await llmCall(prompt);
|
|
131
|
+
if (summary && summary.length > 50) {
|
|
132
|
+
// Insert summary as a new chunk
|
|
133
|
+
store.insertSummaryChunk(group.topic, `Consolidated Summary (${group.chunkIds.length} chunks)`, summary.slice(0, 3000));
|
|
134
|
+
// Mark source chunks as consolidated
|
|
135
|
+
store.markConsolidated(group.chunkIds);
|
|
136
|
+
summarized += group.chunkIds.length;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
logger.debug({ err, topic: group.topic }, 'Failed to summarize topic group');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return summarized;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Strategy 3: Behavioral corrections that appear 3+ times in session reflections
|
|
147
|
+
* are distilled into permanent rules. Returns the number of new principles created.
|
|
148
|
+
*/
|
|
149
|
+
async function extractPrinciples(store, llmCall) {
|
|
150
|
+
const patterns = store.getBehavioralPatterns(3); // corrections appearing 3+ times
|
|
151
|
+
if (patterns.length === 0)
|
|
152
|
+
return 0;
|
|
153
|
+
const prompt = `The following behavioral corrections have been received repeatedly from the user.\n\n` +
|
|
154
|
+
`${patterns.map(p => `- "${p.correction}" (${p.category}, ${p.count} times)`).join('\n')}\n\n` +
|
|
155
|
+
`Distill these into concise, actionable rules (1 sentence each) that should be ` +
|
|
156
|
+
`permanently applied. Merge similar corrections into a single rule.\n\n` +
|
|
157
|
+
`Output only the rules as a numbered list, nothing else.`;
|
|
158
|
+
try {
|
|
159
|
+
const rules = await llmCall(prompt);
|
|
160
|
+
if (rules && rules.length > 20) {
|
|
161
|
+
// Store as a consolidated principle chunk
|
|
162
|
+
store.insertSummaryChunk('00-System/MEMORY', 'Behavioral Principles (auto-consolidated)', rules.slice(0, 2000));
|
|
163
|
+
return patterns.length;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
logger.debug({ err }, 'Failed to extract behavioral principles');
|
|
168
|
+
}
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=consolidation.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Token-Budgeted Context Assembler.
|
|
3
|
+
*
|
|
4
|
+
* Assembles context for system prompt injection within a strict character budget.
|
|
5
|
+
* Fills slots by priority so the most important context always fits, with
|
|
6
|
+
* graceful degradation when budget is tight.
|
|
7
|
+
*
|
|
8
|
+
* Inspired by Phantom's MemoryContextBuilder pattern.
|
|
9
|
+
*/
|
|
10
|
+
import type { SearchResult } from '../types.js';
|
|
11
|
+
export interface AssembledContext {
|
|
12
|
+
/** Combined context string ready for prompt injection. */
|
|
13
|
+
text: string;
|
|
14
|
+
/** Total characters used. */
|
|
15
|
+
charsUsed: number;
|
|
16
|
+
/** Which slots were included. */
|
|
17
|
+
slotsIncluded: string[];
|
|
18
|
+
/** Which slots were skipped (due to budget or empty). */
|
|
19
|
+
slotsSkipped: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface AssemblerOptions {
|
|
22
|
+
/** Total character budget for all context. Default: 12000. */
|
|
23
|
+
totalBudget?: number;
|
|
24
|
+
/** Working memory file path (null to skip). */
|
|
25
|
+
workingMemoryPath?: string | null;
|
|
26
|
+
/** Pre-searched memory results. */
|
|
27
|
+
memoryResults?: SearchResult[];
|
|
28
|
+
/** Pre-resolved skill context string. */
|
|
29
|
+
skillContext?: string;
|
|
30
|
+
/** Pre-resolved graph context string. */
|
|
31
|
+
graphContext?: string;
|
|
32
|
+
/** Whether this is an autonomous run (truncates aggressively). */
|
|
33
|
+
isAutonomous?: boolean;
|
|
34
|
+
/** Identity file path (null to skip). */
|
|
35
|
+
identityPath?: string | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Assemble context from multiple sources within a character budget.
|
|
39
|
+
*
|
|
40
|
+
* Priority order:
|
|
41
|
+
* 1. Working memory (persistent scratchpad — always relevant)
|
|
42
|
+
* 2. Procedural skills (high-value, rarely large)
|
|
43
|
+
* 3. Memory search results (core recall)
|
|
44
|
+
* 4. Graph relationships (supplementary enrichment)
|
|
45
|
+
*
|
|
46
|
+
* Each slot truncates independently to its own maxChars limit.
|
|
47
|
+
* If total budget is exhausted, lower-priority slots are skipped entirely.
|
|
48
|
+
*/
|
|
49
|
+
export declare function assembleContext(options: AssemblerOptions): Promise<AssembledContext>;
|
|
50
|
+
//# sourceMappingURL=context-assembler.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Token-Budgeted Context Assembler.
|
|
3
|
+
*
|
|
4
|
+
* Assembles context for system prompt injection within a strict character budget.
|
|
5
|
+
* Fills slots by priority so the most important context always fits, with
|
|
6
|
+
* graceful degradation when budget is tight.
|
|
7
|
+
*
|
|
8
|
+
* Inspired by Phantom's MemoryContextBuilder pattern.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import { formatResultsForPrompt } from './search.js';
|
|
12
|
+
/**
|
|
13
|
+
* Assemble context from multiple sources within a character budget.
|
|
14
|
+
*
|
|
15
|
+
* Priority order:
|
|
16
|
+
* 1. Working memory (persistent scratchpad — always relevant)
|
|
17
|
+
* 2. Procedural skills (high-value, rarely large)
|
|
18
|
+
* 3. Memory search results (core recall)
|
|
19
|
+
* 4. Graph relationships (supplementary enrichment)
|
|
20
|
+
*
|
|
21
|
+
* Each slot truncates independently to its own maxChars limit.
|
|
22
|
+
* If total budget is exhausted, lower-priority slots are skipped entirely.
|
|
23
|
+
*/
|
|
24
|
+
export async function assembleContext(options) {
|
|
25
|
+
const totalBudget = options.totalBudget ?? 12_000;
|
|
26
|
+
const isAutonomous = options.isAutonomous ?? false;
|
|
27
|
+
const slots = [];
|
|
28
|
+
// Slot 0: Identity seed (always loaded, tiny footprint)
|
|
29
|
+
if (options.identityPath) {
|
|
30
|
+
const idPath = options.identityPath;
|
|
31
|
+
slots.push({
|
|
32
|
+
name: 'identity',
|
|
33
|
+
priority: 0,
|
|
34
|
+
maxChars: 500,
|
|
35
|
+
minRemainingBudget: 0,
|
|
36
|
+
resolve: () => {
|
|
37
|
+
if (!fs.existsSync(idPath))
|
|
38
|
+
return '';
|
|
39
|
+
try {
|
|
40
|
+
const content = fs.readFileSync(idPath, 'utf-8').trim();
|
|
41
|
+
if (!content)
|
|
42
|
+
return '';
|
|
43
|
+
return `## Identity\n\n${content}`;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Slot 1: Working memory (highest priority — always fits)
|
|
52
|
+
if (options.workingMemoryPath) {
|
|
53
|
+
const wmPath = options.workingMemoryPath;
|
|
54
|
+
slots.push({
|
|
55
|
+
name: 'working-memory',
|
|
56
|
+
priority: 1,
|
|
57
|
+
maxChars: isAutonomous ? 1000 : 2000,
|
|
58
|
+
minRemainingBudget: 0,
|
|
59
|
+
resolve: () => {
|
|
60
|
+
if (!fs.existsSync(wmPath))
|
|
61
|
+
return '';
|
|
62
|
+
try {
|
|
63
|
+
const content = fs.readFileSync(wmPath, 'utf-8').trim();
|
|
64
|
+
if (!content)
|
|
65
|
+
return '';
|
|
66
|
+
return `## Working Memory (scratchpad)\n\n${content}`;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Slot 2: Procedural skills (high value, small footprint)
|
|
75
|
+
if (options.skillContext) {
|
|
76
|
+
const skillCtx = options.skillContext;
|
|
77
|
+
slots.push({
|
|
78
|
+
name: 'skills',
|
|
79
|
+
priority: 2,
|
|
80
|
+
maxChars: isAutonomous ? 1000 : 2000,
|
|
81
|
+
minRemainingBudget: 500,
|
|
82
|
+
resolve: () => skillCtx,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Slot 3: Memory search results (core recall)
|
|
86
|
+
if (options.memoryResults && options.memoryResults.length > 0) {
|
|
87
|
+
const results = options.memoryResults;
|
|
88
|
+
slots.push({
|
|
89
|
+
name: 'memory-recall',
|
|
90
|
+
priority: 3,
|
|
91
|
+
maxChars: isAutonomous ? 2000 : 8000,
|
|
92
|
+
minRemainingBudget: 200,
|
|
93
|
+
resolve: () => {
|
|
94
|
+
// formatResultsForPrompt already handles truncation within its own budget
|
|
95
|
+
return formatResultsForPrompt(results, isAutonomous ? 2000 : 8000);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Slot 4: Graph relationships (supplementary)
|
|
100
|
+
if (options.graphContext) {
|
|
101
|
+
const graphCtx = options.graphContext;
|
|
102
|
+
slots.push({
|
|
103
|
+
name: 'graph',
|
|
104
|
+
priority: 4,
|
|
105
|
+
maxChars: 2000,
|
|
106
|
+
minRemainingBudget: 500,
|
|
107
|
+
resolve: () => graphCtx,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Sort by priority (lower number = higher priority)
|
|
111
|
+
slots.sort((a, b) => a.priority - b.priority);
|
|
112
|
+
// Fill slots within budget
|
|
113
|
+
let remaining = totalBudget;
|
|
114
|
+
const parts = [];
|
|
115
|
+
const included = [];
|
|
116
|
+
const skipped = [];
|
|
117
|
+
for (const slot of slots) {
|
|
118
|
+
// Check if we have enough remaining budget
|
|
119
|
+
if (remaining < slot.minRemainingBudget) {
|
|
120
|
+
skipped.push(slot.name);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
let content = await slot.resolve();
|
|
125
|
+
if (!content) {
|
|
126
|
+
skipped.push(slot.name);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// Truncate to the smaller of slot max and remaining budget
|
|
130
|
+
const limit = Math.min(slot.maxChars, remaining);
|
|
131
|
+
if (content.length > limit) {
|
|
132
|
+
content = content.slice(0, limit) + '\n...(truncated)';
|
|
133
|
+
}
|
|
134
|
+
parts.push(content);
|
|
135
|
+
remaining -= content.length;
|
|
136
|
+
included.push(slot.name);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
skipped.push(slot.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
text: parts.join('\n\n'),
|
|
144
|
+
charsUsed: totalBudget - remaining,
|
|
145
|
+
slotsIncluded: included,
|
|
146
|
+
slotsSkipped: skipped,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=context-assembler.js.map
|