clementine-agent 1.0.1 → 1.0.3
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/README.md +7 -0
- package/dist/agent/assistant.d.ts +13 -0
- package/dist/agent/assistant.js +76 -5
- package/dist/cli/chat.js +28 -5
- package/dist/cli/index.js +125 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +2 -2
- package/dist/gateway/router.d.ts +5 -0
- package/dist/gateway/router.js +4 -0
- package/dist/index.js +29 -0
- package/dist/memory/maintenance.d.ts +20 -0
- package/dist/memory/maintenance.js +121 -0
- package/dist/memory/store.js +14 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,8 @@ bash install.sh
|
|
|
85
85
|
|
|
86
86
|
The install script handles everything: system dependencies (redis, libomp, build tools), npm packages, TypeScript build, global CLI install, and launches the setup wizard. Safe to re-run — skips anything already installed.
|
|
87
87
|
|
|
88
|
+
The setup wizard auto-generates a Discord bot invite URL from your token and offers to open it in your browser — no need to visit the Developer Portal manually.
|
|
89
|
+
|
|
88
90
|
After setup:
|
|
89
91
|
|
|
90
92
|
```bash
|
|
@@ -324,6 +326,11 @@ clementine tools List available MCP tools, plugins, and channels
|
|
|
324
326
|
clementine config setup Interactive configuration wizard
|
|
325
327
|
clementine config set KEY VAL Set a single config value
|
|
326
328
|
clementine config get KEY Read a config value
|
|
329
|
+
clementine config edit Open .env in your editor ($EDITOR)
|
|
330
|
+
clementine memory search <q> Search memory from the terminal (FTS5)
|
|
331
|
+
clementine projects list Show all linked projects
|
|
332
|
+
clementine projects add <path> Link a project directory (-d desc, -k keywords)
|
|
333
|
+
clementine projects remove <p> Unlink a project directory
|
|
327
334
|
clementine cron list List all cron jobs and last run status
|
|
328
335
|
clementine cron run <job> Run a specific cron job
|
|
329
336
|
clementine cron run-due Run all due jobs (for OS scheduler)
|
|
@@ -26,6 +26,10 @@ export interface ProjectMeta {
|
|
|
26
26
|
export declare function findProjectByName(query: string): ProjectMeta | null;
|
|
27
27
|
/** Get all linked projects. */
|
|
28
28
|
export declare function getLinkedProjects(): ProjectMeta[];
|
|
29
|
+
/** Add a project to the linked projects list. */
|
|
30
|
+
export declare function addProject(projectPath: string, description?: string, keywords?: string[]): void;
|
|
31
|
+
/** Remove a project from the linked projects list. Returns true if removed. */
|
|
32
|
+
export declare function removeProject(projectPath: string): boolean;
|
|
29
33
|
export declare class PersonalAssistant {
|
|
30
34
|
static readonly MAX_SESSION_EXCHANGES = 40;
|
|
31
35
|
private sessions;
|
|
@@ -50,6 +54,8 @@ export declare class PersonalAssistant {
|
|
|
50
54
|
/** Per-session stall nudge — set after a query shows stall signals, consumed on the next query. */
|
|
51
55
|
private stallNudges;
|
|
52
56
|
private _compactedSessions;
|
|
57
|
+
/** Last auto-matched project per session — exposed for CLI display. */
|
|
58
|
+
private _lastMatchedProject;
|
|
53
59
|
/** Hot correction buffer — explicit behavioral corrections applied before nightly SI. */
|
|
54
60
|
private hotCorrections;
|
|
55
61
|
constructor();
|
|
@@ -113,6 +119,11 @@ export declare class PersonalAssistant {
|
|
|
113
119
|
* No LLM call — uses buildLocalSummary for instant summarization.
|
|
114
120
|
*/
|
|
115
121
|
private compactContext;
|
|
122
|
+
/**
|
|
123
|
+
* Expire sessions inactive for more than 24 hours.
|
|
124
|
+
* Called periodically from chat() to prevent unbounded map growth.
|
|
125
|
+
*/
|
|
126
|
+
private expireOldSessions;
|
|
116
127
|
/**
|
|
117
128
|
* Build an instant local summary from in-memory exchange history.
|
|
118
129
|
* No LLM call — returns immediately. Used during session rotation
|
|
@@ -220,6 +231,8 @@ export declare class PersonalAssistant {
|
|
|
220
231
|
content: string;
|
|
221
232
|
}>>;
|
|
222
233
|
clearSession(sessionKey: string): void;
|
|
234
|
+
/** Get the last auto-matched project for a session (for CLI display). */
|
|
235
|
+
getLastMatchedProject(sessionKey: string): ProjectMeta | null;
|
|
223
236
|
getProfileManager(): AgentManager;
|
|
224
237
|
}
|
|
225
238
|
//# sourceMappingURL=assistant.d.ts.map
|
package/dist/agent/assistant.js
CHANGED
|
@@ -469,6 +469,33 @@ export function findProjectByName(query) {
|
|
|
469
469
|
export function getLinkedProjects() {
|
|
470
470
|
return loadProjectsMeta();
|
|
471
471
|
}
|
|
472
|
+
/** Add a project to the linked projects list. */
|
|
473
|
+
export function addProject(projectPath, description, keywords) {
|
|
474
|
+
const resolved = path.resolve(projectPath);
|
|
475
|
+
const projects = loadProjectsMeta();
|
|
476
|
+
// Avoid duplicates
|
|
477
|
+
if (projects.some(p => path.resolve(p.path) === resolved))
|
|
478
|
+
return;
|
|
479
|
+
const entry = { path: resolved };
|
|
480
|
+
if (description)
|
|
481
|
+
entry.description = description;
|
|
482
|
+
if (keywords?.length)
|
|
483
|
+
entry.keywords = keywords;
|
|
484
|
+
projects.push(entry);
|
|
485
|
+
fs.writeFileSync(PROJECTS_META_FILE, JSON.stringify(projects, null, 4));
|
|
486
|
+
_projectsMetaCacheTime = 0; // invalidate cache
|
|
487
|
+
}
|
|
488
|
+
/** Remove a project from the linked projects list. Returns true if removed. */
|
|
489
|
+
export function removeProject(projectPath) {
|
|
490
|
+
const resolved = path.resolve(projectPath);
|
|
491
|
+
const projects = loadProjectsMeta();
|
|
492
|
+
const filtered = projects.filter(p => path.resolve(p.path) !== resolved);
|
|
493
|
+
if (filtered.length === projects.length)
|
|
494
|
+
return false;
|
|
495
|
+
fs.writeFileSync(PROJECTS_META_FILE, JSON.stringify(filtered, null, 4));
|
|
496
|
+
_projectsMetaCacheTime = 0; // invalidate cache
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
472
499
|
// ── PersonalAssistant ───────────────────────────────────────────────
|
|
473
500
|
export class PersonalAssistant {
|
|
474
501
|
static MAX_SESSION_EXCHANGES = MAX_SESSION_EXCHANGES;
|
|
@@ -494,6 +521,8 @@ export class PersonalAssistant {
|
|
|
494
521
|
/** Per-session stall nudge — set after a query shows stall signals, consumed on the next query. */
|
|
495
522
|
stallNudges = new Map();
|
|
496
523
|
_compactedSessions = new Set();
|
|
524
|
+
/** Last auto-matched project per session — exposed for CLI display. */
|
|
525
|
+
_lastMatchedProject = new Map();
|
|
497
526
|
/** Hot correction buffer — explicit behavioral corrections applied before nightly SI. */
|
|
498
527
|
hotCorrections = [];
|
|
499
528
|
constructor() {
|
|
@@ -1482,6 +1511,8 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1482
1511
|
const key = sessionKey ?? undefined;
|
|
1483
1512
|
this._lastUserMessage = text;
|
|
1484
1513
|
let sessionRotated = false;
|
|
1514
|
+
// Periodic cleanup: expire all sessions older than 24 hours
|
|
1515
|
+
this.expireOldSessions();
|
|
1485
1516
|
// Expire old sessions (4 hours)
|
|
1486
1517
|
if (key && this.sessionTimestamps.has(key)) {
|
|
1487
1518
|
const elapsed = Date.now() - this.sessionTimestamps.get(key).getTime();
|
|
@@ -1735,6 +1766,9 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1735
1766
|
setSendPolicy(profile?.sendPolicy ?? null, profile?.slug ?? null);
|
|
1736
1767
|
setAgentDir(profile?.agentDir ?? null);
|
|
1737
1768
|
setInteractionSource(inferInteractionSource(sessionKey));
|
|
1769
|
+
// Track the matched project for CLI display
|
|
1770
|
+
if (sessionKey)
|
|
1771
|
+
this._lastMatchedProject.set(sessionKey, matchedProject);
|
|
1738
1772
|
if (matchedProject) {
|
|
1739
1773
|
logger.info({ project: matchedProject.path }, 'Auto-matched project from message');
|
|
1740
1774
|
const projName = path.basename(matchedProject.path);
|
|
@@ -2133,13 +2167,17 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2133
2167
|
return;
|
|
2134
2168
|
// Build compaction block for working memory
|
|
2135
2169
|
const exchangeCount = this.exchangeCounts.get(sessionKey) ?? 0;
|
|
2170
|
+
const COMPACTION_START = '<!-- COMPACTION_START -->';
|
|
2171
|
+
const COMPACTION_END = '<!-- COMPACTION_END -->';
|
|
2136
2172
|
const compactionBlock = [
|
|
2173
|
+
COMPACTION_START,
|
|
2137
2174
|
`## Session Compaction (auto-generated)`,
|
|
2138
2175
|
`Session ${sessionKey} compacted at ${exchangeCount} exchanges.`,
|
|
2139
2176
|
``,
|
|
2140
2177
|
summary,
|
|
2141
2178
|
``,
|
|
2142
2179
|
`*Continue from where this conversation left off.*`,
|
|
2180
|
+
COMPACTION_END,
|
|
2143
2181
|
].join('\n');
|
|
2144
2182
|
// Write to working memory so the next session picks it up via system prompt
|
|
2145
2183
|
try {
|
|
@@ -2150,11 +2188,23 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2150
2188
|
const existing = fs.existsSync(compactionWmFile)
|
|
2151
2189
|
? fs.readFileSync(compactionWmFile, 'utf-8')
|
|
2152
2190
|
: '';
|
|
2153
|
-
// Replace any prior compaction block,
|
|
2154
|
-
const
|
|
2155
|
-
const
|
|
2156
|
-
|
|
2157
|
-
|
|
2191
|
+
// Replace any prior compaction block (try new sentinel format first, then legacy)
|
|
2192
|
+
const sentinelRegex = /<!-- COMPACTION_START -->[\s\S]*?<!-- COMPACTION_END -->/;
|
|
2193
|
+
const legacyRegex = /## Session Compaction \(auto-generated\)[\s\S]*?\*Continue from where this conversation left off\.\*/;
|
|
2194
|
+
let updated;
|
|
2195
|
+
if (sentinelRegex.test(existing)) {
|
|
2196
|
+
updated = existing.replace(sentinelRegex, compactionBlock);
|
|
2197
|
+
}
|
|
2198
|
+
else if (legacyRegex.test(existing)) {
|
|
2199
|
+
updated = existing.replace(legacyRegex, compactionBlock);
|
|
2200
|
+
}
|
|
2201
|
+
else {
|
|
2202
|
+
updated = existing.trimEnd() + '\n\n' + compactionBlock;
|
|
2203
|
+
}
|
|
2204
|
+
// Size guard: if working memory exceeds 10KB, keep only the compaction block
|
|
2205
|
+
if (Buffer.byteLength(updated) > 10_240) {
|
|
2206
|
+
updated = compactionBlock;
|
|
2207
|
+
}
|
|
2158
2208
|
fs.writeFileSync(compactionWmFile, updated);
|
|
2159
2209
|
}
|
|
2160
2210
|
catch {
|
|
@@ -2164,8 +2214,24 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2164
2214
|
// The working memory summary will provide continuity
|
|
2165
2215
|
this.sessions.delete(sessionKey);
|
|
2166
2216
|
this.exchangeCounts.set(sessionKey, 0);
|
|
2217
|
+
this.lastExchanges.delete(sessionKey);
|
|
2218
|
+
this.sessionTimestamps.delete(sessionKey);
|
|
2219
|
+
this.stallNudges.delete(sessionKey);
|
|
2167
2220
|
this.saveSessions();
|
|
2168
2221
|
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Expire sessions inactive for more than 24 hours.
|
|
2224
|
+
* Called periodically from chat() to prevent unbounded map growth.
|
|
2225
|
+
*/
|
|
2226
|
+
expireOldSessions() {
|
|
2227
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
2228
|
+
const now = Date.now();
|
|
2229
|
+
for (const [key, ts] of this.sessionTimestamps) {
|
|
2230
|
+
if (now - ts.getTime() > MAX_AGE_MS) {
|
|
2231
|
+
this.clearSession(key);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2169
2235
|
// ── Session Summarization ─────────────────────────────────────────
|
|
2170
2236
|
/**
|
|
2171
2237
|
* Build an instant local summary from in-memory exchange history.
|
|
@@ -3879,8 +3945,13 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3879
3945
|
this.sessionTimestamps.delete(sessionKey);
|
|
3880
3946
|
this.lastExchanges.delete(sessionKey);
|
|
3881
3947
|
this.stallNudges.delete(sessionKey);
|
|
3948
|
+
this._lastMatchedProject.delete(sessionKey);
|
|
3882
3949
|
this.saveSessions();
|
|
3883
3950
|
}
|
|
3951
|
+
/** Get the last auto-matched project for a session (for CLI display). */
|
|
3952
|
+
getLastMatchedProject(sessionKey) {
|
|
3953
|
+
return this._lastMatchedProject.get(sessionKey) ?? null;
|
|
3954
|
+
}
|
|
3884
3955
|
getProfileManager() {
|
|
3885
3956
|
return this.profileManager;
|
|
3886
3957
|
}
|
package/dist/cli/chat.js
CHANGED
|
@@ -198,12 +198,35 @@ export async function cmdChat(opts) {
|
|
|
198
198
|
}
|
|
199
199
|
// ── Send message ──────────────────────────────────────────
|
|
200
200
|
process.stdout.write(`\n${DIM}thinking...${RESET}\r`);
|
|
201
|
+
let firstToken = true;
|
|
202
|
+
let streamedLen = 0;
|
|
201
203
|
try {
|
|
202
|
-
const response = await gateway.handleMessage(sessionKey, effectiveText,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
const response = await gateway.handleMessage(sessionKey, effectiveText, async (token) => {
|
|
205
|
+
if (firstToken) {
|
|
206
|
+
// Clear "thinking..." and show project context on first real token
|
|
207
|
+
process.stdout.write('\x1b[2K\r');
|
|
208
|
+
const matched = gateway.getLastMatchedProject(sessionKey);
|
|
209
|
+
if (matched) {
|
|
210
|
+
process.stdout.write(`${DIM}[project: ${path.basename(matched.path)}]${RESET}\n`);
|
|
211
|
+
}
|
|
212
|
+
firstToken = false;
|
|
213
|
+
}
|
|
214
|
+
process.stdout.write(token);
|
|
215
|
+
streamedLen += token.length;
|
|
216
|
+
}, oneOffModel);
|
|
217
|
+
// If we streamed, just add a newline. Otherwise fall back to full render.
|
|
218
|
+
if (streamedLen > 0) {
|
|
219
|
+
process.stdout.write('\n\n');
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
process.stdout.write('\x1b[2K\r');
|
|
223
|
+
const matched = gateway.getLastMatchedProject(sessionKey);
|
|
224
|
+
if (matched) {
|
|
225
|
+
console.log(`${DIM}[project: ${path.basename(matched.path)}]${RESET}`);
|
|
226
|
+
}
|
|
227
|
+
console.log(renderMarkdown(response));
|
|
228
|
+
console.log();
|
|
229
|
+
}
|
|
207
230
|
}
|
|
208
231
|
catch (err) {
|
|
209
232
|
process.stdout.write('\x1b[2K\r');
|
package/dist/cli/index.js
CHANGED
|
@@ -1295,6 +1295,131 @@ configCmd
|
|
|
1295
1295
|
.command('list')
|
|
1296
1296
|
.description('List all config values')
|
|
1297
1297
|
.action(cmdConfigList);
|
|
1298
|
+
configCmd
|
|
1299
|
+
.command('edit')
|
|
1300
|
+
.description('Open .env in your editor')
|
|
1301
|
+
.action(() => {
|
|
1302
|
+
if (!existsSync(ENV_PATH)) {
|
|
1303
|
+
console.log(' No .env file found. Run: clementine config setup');
|
|
1304
|
+
process.exit(1);
|
|
1305
|
+
}
|
|
1306
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
1307
|
+
try {
|
|
1308
|
+
execSync(`${editor} "${ENV_PATH}"`, { stdio: 'inherit' });
|
|
1309
|
+
}
|
|
1310
|
+
catch {
|
|
1311
|
+
console.error(` Failed to open editor: ${editor}`);
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
// ── Memory commands ─────────────────────────────────────────────────
|
|
1315
|
+
const memoryCmd = program
|
|
1316
|
+
.command('memory')
|
|
1317
|
+
.description('Search and manage memory');
|
|
1318
|
+
memoryCmd
|
|
1319
|
+
.command('search <query>')
|
|
1320
|
+
.description('Search memory (full-text)')
|
|
1321
|
+
.option('-n, --limit <n>', 'Max results', '10')
|
|
1322
|
+
.action(async (query, opts) => {
|
|
1323
|
+
const DIM = '\x1b[0;90m';
|
|
1324
|
+
const BOLD = '\x1b[1m';
|
|
1325
|
+
const CYAN = '\x1b[0;36m';
|
|
1326
|
+
const RESET = '\x1b[0m';
|
|
1327
|
+
try {
|
|
1328
|
+
const { MemoryStore } = await import('../memory/store.js');
|
|
1329
|
+
const VAULT_DIR = path.join(BASE_DIR, 'vault');
|
|
1330
|
+
const DB_PATH = path.join(VAULT_DIR, '.memory.db');
|
|
1331
|
+
const store = new MemoryStore(DB_PATH, VAULT_DIR);
|
|
1332
|
+
const results = store.searchFts(query, parseInt(opts.limit, 10));
|
|
1333
|
+
if (results.length === 0) {
|
|
1334
|
+
console.log(` No results for "${query}".`);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
console.log();
|
|
1338
|
+
for (const r of results) {
|
|
1339
|
+
const source = r.sourceFile ? path.basename(r.sourceFile) : 'unknown';
|
|
1340
|
+
const section = r.section || '';
|
|
1341
|
+
const snippet = r.content.replace(/\n/g, ' ').slice(0, 120);
|
|
1342
|
+
console.log(` ${BOLD}${source}${RESET}${section ? ` › ${CYAN}${section}${RESET}` : ''}`);
|
|
1343
|
+
console.log(` ${DIM}${snippet}${snippet.length >= 120 ? '…' : ''}${RESET}`);
|
|
1344
|
+
console.log();
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
catch (err) {
|
|
1348
|
+
console.error(` Error searching memory: ${err}`);
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
// ── Projects commands ───────────────────────────────────────────────
|
|
1352
|
+
const projectsCmd = program
|
|
1353
|
+
.command('projects')
|
|
1354
|
+
.description('Manage linked projects');
|
|
1355
|
+
projectsCmd
|
|
1356
|
+
.command('list')
|
|
1357
|
+
.description('Show all linked projects')
|
|
1358
|
+
.action(async () => {
|
|
1359
|
+
const DIM = '\x1b[0;90m';
|
|
1360
|
+
const BOLD = '\x1b[1m';
|
|
1361
|
+
const RESET = '\x1b[0m';
|
|
1362
|
+
try {
|
|
1363
|
+
const { getLinkedProjects } = await import('../agent/assistant.js');
|
|
1364
|
+
const projects = getLinkedProjects();
|
|
1365
|
+
if (projects.length === 0) {
|
|
1366
|
+
console.log(' No projects linked. Use: clementine projects add <path>');
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
console.log();
|
|
1370
|
+
for (const p of projects) {
|
|
1371
|
+
console.log(` ${BOLD}${path.basename(p.path)}${RESET}`);
|
|
1372
|
+
console.log(` ${DIM}${p.path}${RESET}`);
|
|
1373
|
+
if (p.description)
|
|
1374
|
+
console.log(` ${p.description}`);
|
|
1375
|
+
if (p.keywords?.length)
|
|
1376
|
+
console.log(` ${DIM}Keywords: ${p.keywords.join(', ')}${RESET}`);
|
|
1377
|
+
console.log();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
catch (err) {
|
|
1381
|
+
console.error(` Error listing projects: ${err}`);
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
projectsCmd
|
|
1385
|
+
.command('add <path>')
|
|
1386
|
+
.description('Link a project directory')
|
|
1387
|
+
.option('-d, --description <desc>', 'Project description')
|
|
1388
|
+
.option('-k, --keywords <kw>', 'Comma-separated keywords')
|
|
1389
|
+
.action(async (projectPath, opts) => {
|
|
1390
|
+
const resolved = path.resolve(projectPath);
|
|
1391
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
1392
|
+
console.error(` Not a directory: ${resolved}`);
|
|
1393
|
+
process.exit(1);
|
|
1394
|
+
}
|
|
1395
|
+
try {
|
|
1396
|
+
const { addProject } = await import('../agent/assistant.js');
|
|
1397
|
+
const keywords = opts.keywords?.split(',').map(k => k.trim()).filter(Boolean);
|
|
1398
|
+
addProject(resolved, opts.description, keywords);
|
|
1399
|
+
console.log(` Linked: ${resolved}`);
|
|
1400
|
+
}
|
|
1401
|
+
catch (err) {
|
|
1402
|
+
console.error(` Error adding project: ${err}`);
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
projectsCmd
|
|
1406
|
+
.command('remove <path>')
|
|
1407
|
+
.description('Unlink a project directory')
|
|
1408
|
+
.action(async (projectPath) => {
|
|
1409
|
+
const resolved = path.resolve(projectPath);
|
|
1410
|
+
try {
|
|
1411
|
+
const { removeProject } = await import('../agent/assistant.js');
|
|
1412
|
+
if (removeProject(resolved)) {
|
|
1413
|
+
console.log(` Removed: ${resolved}`);
|
|
1414
|
+
}
|
|
1415
|
+
else {
|
|
1416
|
+
console.log(` Not found: ${resolved}`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
catch (err) {
|
|
1420
|
+
console.error(` Error removing project: ${err}`);
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1298
1423
|
// ── Update command ──────────────────────────────────────────────────
|
|
1299
1424
|
async function cmdUpdate(options) {
|
|
1300
1425
|
const DIM = '\x1b[0;90m';
|
package/dist/config.d.ts
CHANGED
|
@@ -117,8 +117,8 @@ export declare const LINK_EXTRACT_MAX_URLS = 3;
|
|
|
117
117
|
export declare const LINK_EXTRACT_MAX_CHARS = 4000;
|
|
118
118
|
export declare const MEMORY_DB_PATH: string;
|
|
119
119
|
export declare const GRAPH_DB_DIR: string;
|
|
120
|
-
export declare const SEARCH_CONTEXT_LIMIT =
|
|
121
|
-
export declare const SEARCH_RECENCY_LIMIT =
|
|
120
|
+
export declare const SEARCH_CONTEXT_LIMIT = 6;
|
|
121
|
+
export declare const SEARCH_RECENCY_LIMIT = 4;
|
|
122
122
|
export declare const SYSTEM_PROMPT_MAX_CONTEXT_CHARS = 12000;
|
|
123
123
|
export declare const SESSION_EXCHANGE_HISTORY_SIZE = 10;
|
|
124
124
|
export declare const SESSION_EXCHANGE_MAX_CHARS = 2000;
|
package/dist/config.js
CHANGED
|
@@ -239,8 +239,8 @@ export const LINK_EXTRACT_MAX_CHARS = 4000;
|
|
|
239
239
|
// ── Memory / Search ──────────────────────────────────────────────────
|
|
240
240
|
export const MEMORY_DB_PATH = path.join(VAULT_DIR, '.memory.db');
|
|
241
241
|
export const GRAPH_DB_DIR = path.join(BASE_DIR, '.graph.db');
|
|
242
|
-
export const SEARCH_CONTEXT_LIMIT =
|
|
243
|
-
export const SEARCH_RECENCY_LIMIT =
|
|
242
|
+
export const SEARCH_CONTEXT_LIMIT = 6;
|
|
243
|
+
export const SEARCH_RECENCY_LIMIT = 4;
|
|
244
244
|
export const SYSTEM_PROMPT_MAX_CONTEXT_CHARS = 12000;
|
|
245
245
|
// ── Session Persistence ──────────────────────────────────────────────
|
|
246
246
|
export const SESSION_EXCHANGE_HISTORY_SIZE = 10;
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -196,6 +196,11 @@ export declare class Gateway {
|
|
|
196
196
|
memoryCount: number;
|
|
197
197
|
};
|
|
198
198
|
clearSession(sessionKey: string): void;
|
|
199
|
+
/** Get the last auto-matched project for a session. */
|
|
200
|
+
getLastMatchedProject(sessionKey: string): {
|
|
201
|
+
path: string;
|
|
202
|
+
description?: string;
|
|
203
|
+
} | null;
|
|
199
204
|
/** Evict stale session entries (no activity in 48h, no active lock). */
|
|
200
205
|
evictStaleSessions(): number;
|
|
201
206
|
/** Get all active session provenance entries (for dashboard/monitoring). */
|
package/dist/gateway/router.js
CHANGED
|
@@ -1195,6 +1195,10 @@ export class Gateway {
|
|
|
1195
1195
|
this.assistant.clearSession(sessionKey);
|
|
1196
1196
|
this.sessions.delete(sessionKey);
|
|
1197
1197
|
}
|
|
1198
|
+
/** Get the last auto-matched project for a session. */
|
|
1199
|
+
getLastMatchedProject(sessionKey) {
|
|
1200
|
+
return this.assistant.getLastMatchedProject(sessionKey);
|
|
1201
|
+
}
|
|
1198
1202
|
/** Evict stale session entries (no activity in 48h, no active lock). */
|
|
1199
1203
|
evictStaleSessions() {
|
|
1200
1204
|
const cutoff = Date.now() - 48 * 60 * 60 * 1000;
|
package/dist/index.js
CHANGED
|
@@ -545,6 +545,33 @@ async function asyncMain() {
|
|
|
545
545
|
// Agent layer
|
|
546
546
|
const { PersonalAssistant } = await import('./agent/assistant.js');
|
|
547
547
|
const assistant = new PersonalAssistant();
|
|
548
|
+
// Memory maintenance — startup + periodic (non-blocking)
|
|
549
|
+
let maintenanceInterval;
|
|
550
|
+
{
|
|
551
|
+
const memStore = assistant.getMemoryStore();
|
|
552
|
+
if (memStore) {
|
|
553
|
+
const { runStartupMaintenance, startPeriodicMaintenance } = await import('./memory/maintenance.js');
|
|
554
|
+
// Fire-and-forget startup maintenance
|
|
555
|
+
runStartupMaintenance(memStore).catch(() => { });
|
|
556
|
+
// Periodic maintenance every 6 hours (consolidation needs an LLM caller)
|
|
557
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
558
|
+
const llmCall = async (prompt) => {
|
|
559
|
+
try {
|
|
560
|
+
let result = '';
|
|
561
|
+
const stream = query({ prompt, options: { model: 'claude-haiku-4-5-20251001', maxTurns: 1, systemPrompt: 'You are a memory consolidation assistant. Be concise.' } });
|
|
562
|
+
for await (const msg of stream) {
|
|
563
|
+
if (msg.type === 'result')
|
|
564
|
+
result = msg.result ?? '';
|
|
565
|
+
}
|
|
566
|
+
return result;
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return '';
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
maintenanceInterval = startPeriodicMaintenance(memStore, llmCall);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
548
575
|
// Gateway layer
|
|
549
576
|
const { Gateway } = await import('./gateway/router.js');
|
|
550
577
|
const gateway = new Gateway(assistant);
|
|
@@ -833,6 +860,8 @@ async function asyncMain() {
|
|
|
833
860
|
clearInterval(timerInterval);
|
|
834
861
|
clearInterval(teamDeliveryInterval);
|
|
835
862
|
clearInterval(sourceEditInterval);
|
|
863
|
+
if (maintenanceInterval)
|
|
864
|
+
clearInterval(maintenanceInterval);
|
|
836
865
|
// Close graph store FIRST — FalkorDBLite's cleanup.js registers an
|
|
837
866
|
// uncaughtException handler that re-throws errors. If a Redis socket
|
|
838
867
|
// drops during the drain wait, that handler crashes the process.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Automatic Memory Maintenance.
|
|
3
|
+
*
|
|
4
|
+
* Runs startup and periodic maintenance so the memory store stays healthy
|
|
5
|
+
* without manual intervention. New users get this out of the box.
|
|
6
|
+
*
|
|
7
|
+
* Startup: decay salience, prune stale data, backfill embeddings
|
|
8
|
+
* Periodic (every 6h): full consolidation cycle + embedding rebuild
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Run one-time maintenance at daemon startup.
|
|
12
|
+
* Non-blocking — errors are logged but never thrown.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runStartupMaintenance(store: any): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Start periodic maintenance on a 6-hour interval.
|
|
17
|
+
* Returns the interval handle for cleanup on shutdown.
|
|
18
|
+
*/
|
|
19
|
+
export declare function startPeriodicMaintenance(store: any, llmCall?: (prompt: string) => Promise<string>): ReturnType<typeof setInterval>;
|
|
20
|
+
//# sourceMappingURL=maintenance.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Automatic Memory Maintenance.
|
|
3
|
+
*
|
|
4
|
+
* Runs startup and periodic maintenance so the memory store stays healthy
|
|
5
|
+
* without manual intervention. New users get this out of the box.
|
|
6
|
+
*
|
|
7
|
+
* Startup: decay salience, prune stale data, backfill embeddings
|
|
8
|
+
* Periodic (every 6h): full consolidation cycle + embedding rebuild
|
|
9
|
+
*/
|
|
10
|
+
import pino from 'pino';
|
|
11
|
+
const logger = pino({ name: 'clementine.maintenance' });
|
|
12
|
+
const PERIODIC_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
13
|
+
/**
|
|
14
|
+
* Run one-time maintenance at daemon startup.
|
|
15
|
+
* Non-blocking — errors are logged but never thrown.
|
|
16
|
+
*/
|
|
17
|
+
export async function runStartupMaintenance(store) {
|
|
18
|
+
const start = Date.now();
|
|
19
|
+
logger.info('Starting memory maintenance (startup)');
|
|
20
|
+
try {
|
|
21
|
+
const decayed = store.decaySalience?.();
|
|
22
|
+
if (decayed)
|
|
23
|
+
logger.info({ decayed }, 'Salience decay applied');
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
logger.warn({ err }, 'Salience decay failed');
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const pruned = store.pruneStaleData?.();
|
|
30
|
+
if (pruned)
|
|
31
|
+
logger.info(pruned, 'Stale data pruned');
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
logger.warn({ err }, 'Stale data pruning failed');
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const embedded = store.buildEmbeddings?.();
|
|
38
|
+
if (embedded)
|
|
39
|
+
logger.info(embedded, 'Embeddings built/backfilled');
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
logger.warn({ err }, 'Embedding backfill failed');
|
|
43
|
+
}
|
|
44
|
+
// Prune old extraction logs (keep active extractions regardless of age)
|
|
45
|
+
try {
|
|
46
|
+
const conn = store.conn;
|
|
47
|
+
if (conn) {
|
|
48
|
+
const result = conn.prepare(`DELETE FROM memory_extractions
|
|
49
|
+
WHERE extracted_at < datetime('now', '-90 days')
|
|
50
|
+
AND status != 'active'`).run();
|
|
51
|
+
if (result.changes > 0) {
|
|
52
|
+
logger.info({ pruned: result.changes }, 'Old extraction logs pruned');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Table may not exist yet — non-fatal
|
|
58
|
+
}
|
|
59
|
+
logger.info({ durationMs: Date.now() - start }, 'Startup maintenance complete');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Start periodic maintenance on a 6-hour interval.
|
|
63
|
+
* Returns the interval handle for cleanup on shutdown.
|
|
64
|
+
*/
|
|
65
|
+
export function startPeriodicMaintenance(store, llmCall) {
|
|
66
|
+
const runCycle = async () => {
|
|
67
|
+
const start = Date.now();
|
|
68
|
+
logger.info('Starting periodic memory maintenance');
|
|
69
|
+
// 1. Decay + prune
|
|
70
|
+
try {
|
|
71
|
+
store.decaySalience?.();
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
logger.warn({ err }, 'Periodic decay failed');
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
store.pruneStaleData?.();
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
logger.warn({ err }, 'Periodic prune failed');
|
|
81
|
+
}
|
|
82
|
+
// 2. Rebuild vocab + backfill embeddings
|
|
83
|
+
try {
|
|
84
|
+
store.buildEmbeddings?.();
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger.warn({ err }, 'Periodic embedding build failed');
|
|
88
|
+
}
|
|
89
|
+
// 3. Consolidation (dedup, summarize, extract principles)
|
|
90
|
+
if (llmCall) {
|
|
91
|
+
try {
|
|
92
|
+
const { runConsolidation } = await import('./consolidation.js');
|
|
93
|
+
const result = await runConsolidation(store, llmCall);
|
|
94
|
+
logger.info(result, 'Consolidation cycle complete');
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
logger.warn({ err }, 'Consolidation failed');
|
|
98
|
+
}
|
|
99
|
+
// 4. Re-backfill embeddings for any new summary chunks from consolidation
|
|
100
|
+
try {
|
|
101
|
+
store.buildEmbeddings?.();
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
logger.warn({ err }, 'Post-consolidation embedding build failed');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 5. Extraction log pruning
|
|
108
|
+
try {
|
|
109
|
+
const conn = store.conn;
|
|
110
|
+
if (conn) {
|
|
111
|
+
conn.prepare(`DELETE FROM memory_extractions
|
|
112
|
+
WHERE extracted_at < datetime('now', '-90 days')
|
|
113
|
+
AND status != 'active'`).run();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch { /* non-fatal */ }
|
|
117
|
+
logger.info({ durationMs: Date.now() - start }, 'Periodic maintenance complete');
|
|
118
|
+
};
|
|
119
|
+
return setInterval(runCycle, PERIODIC_INTERVAL_MS);
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=maintenance.js.map
|
package/dist/memory/store.js
CHANGED
|
@@ -748,9 +748,7 @@ export class MemoryStore {
|
|
|
748
748
|
const rows = this.conn
|
|
749
749
|
.prepare(`SELECT id, source_file, section, content, chunk_type, embedding, salience, agent_slug, updated_at, category, topic
|
|
750
750
|
FROM chunks
|
|
751
|
-
WHERE embedding IS NOT NULL
|
|
752
|
-
ORDER BY updated_at DESC
|
|
753
|
-
LIMIT 500`)
|
|
751
|
+
WHERE embedding IS NOT NULL`)
|
|
754
752
|
.all();
|
|
755
753
|
const scored = [];
|
|
756
754
|
for (const row of rows) {
|
|
@@ -1616,10 +1614,18 @@ export class MemoryStore {
|
|
|
1616
1614
|
*/
|
|
1617
1615
|
insertSummaryChunk(sourceFile, section, content) {
|
|
1618
1616
|
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
1619
|
-
this.conn
|
|
1617
|
+
const result = this.conn
|
|
1620
1618
|
.prepare(`INSERT INTO chunks (source_file, section, content, chunk_type, content_hash, salience, consolidated)
|
|
1621
1619
|
VALUES (?, ?, ?, 'summary', ?, 0.8, 0)`)
|
|
1622
1620
|
.run(sourceFile, section, content, hash);
|
|
1621
|
+
// Immediately compute embedding so the summary is vector-searchable right away
|
|
1622
|
+
if (embeddingsModule.isReady()) {
|
|
1623
|
+
const vec = embeddingsModule.embed(content);
|
|
1624
|
+
if (vec) {
|
|
1625
|
+
this.conn.prepare('UPDATE chunks SET embedding = ? WHERE id = ?')
|
|
1626
|
+
.run(embeddingsModule.serializeEmbedding(vec), result.lastInsertRowid);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1623
1629
|
}
|
|
1624
1630
|
// ── SDR Operational Data ─────────────────────────────────────────
|
|
1625
1631
|
// -- Leads --
|
|
@@ -1912,17 +1918,17 @@ export class MemoryStore {
|
|
|
1912
1918
|
buildEmbeddings() {
|
|
1913
1919
|
// Gather all chunk contents for vocabulary building
|
|
1914
1920
|
const rows = this.conn
|
|
1915
|
-
.prepare('SELECT id, content FROM chunks
|
|
1921
|
+
.prepare('SELECT id, content FROM chunks')
|
|
1916
1922
|
.all();
|
|
1917
1923
|
if (rows.length === 0)
|
|
1918
1924
|
return { vocabSize: 0, backfilled: 0 };
|
|
1919
|
-
// Build vocabulary from corpus
|
|
1925
|
+
// Build vocabulary from entire corpus (including consolidated summaries)
|
|
1920
1926
|
embeddingsModule.buildVocab(rows.map((r) => r.content));
|
|
1921
1927
|
if (!embeddingsModule.isReady())
|
|
1922
1928
|
return { vocabSize: 0, backfilled: 0 };
|
|
1923
|
-
// Backfill embeddings for chunks that don't have one
|
|
1929
|
+
// Backfill embeddings for all chunks that don't have one
|
|
1924
1930
|
const missing = this.conn
|
|
1925
|
-
.prepare('SELECT id, content FROM chunks WHERE embedding IS NULL
|
|
1931
|
+
.prepare('SELECT id, content FROM chunks WHERE embedding IS NULL')
|
|
1926
1932
|
.all();
|
|
1927
1933
|
const updateStmt = this.conn.prepare('UPDATE chunks SET embedding = ? WHERE id = ?');
|
|
1928
1934
|
let backfilled = 0;
|