clementine-agent 1.0.2 → 1.0.4
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/daily-planner.js +19 -61
- package/dist/agent/strategic-planner.js +17 -60
- package/dist/cli/chat.js +27 -9
- package/dist/cli/index.js +177 -39
- package/dist/cli/setup.js +8 -7
- package/dist/config.d.ts +2 -2
- package/dist/config.js +2 -2
- package/dist/gateway/cron-scheduler.js +13 -0
- package/dist/gateway/heartbeat-scheduler.js +1 -1
- 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)
|
|
@@ -8,54 +8,9 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import pino from 'pino';
|
|
11
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
12
11
|
import { BASE_DIR, GOALS_DIR, CRON_REFLECTIONS_DIR, TASKS_FILE, INBOX_DIR, MODELS, } from '../config.js';
|
|
13
12
|
const logger = pino({ name: 'clementine.daily-planner' });
|
|
14
13
|
const PLANS_DIR = path.join(BASE_DIR, 'plans', 'daily');
|
|
15
|
-
// ── .env reader (self-contained — no config.ts secret imports) ───────
|
|
16
|
-
function getEnvValue(key) {
|
|
17
|
-
// Check process env first (already loaded by the daemon)
|
|
18
|
-
if (process.env[key])
|
|
19
|
-
return process.env[key];
|
|
20
|
-
// Fall back to .env file
|
|
21
|
-
const envPath = path.join(BASE_DIR, '.env');
|
|
22
|
-
if (!existsSync(envPath))
|
|
23
|
-
return '';
|
|
24
|
-
for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
|
|
25
|
-
const trimmed = line.trim();
|
|
26
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
27
|
-
continue;
|
|
28
|
-
const eqIndex = trimmed.indexOf('=');
|
|
29
|
-
if (eqIndex === -1)
|
|
30
|
-
continue;
|
|
31
|
-
if (trimmed.slice(0, eqIndex) !== key)
|
|
32
|
-
continue;
|
|
33
|
-
let value = trimmed.slice(eqIndex + 1);
|
|
34
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
35
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
36
|
-
value = value.slice(1, -1);
|
|
37
|
-
}
|
|
38
|
-
return value;
|
|
39
|
-
}
|
|
40
|
-
return '';
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Build Anthropic client credentials.
|
|
44
|
-
* Priority: ANTHROPIC_AUTH_TOKEN (OAuth) > ANTHROPIC_API_KEY (legacy raw key).
|
|
45
|
-
* Returns null if neither is configured.
|
|
46
|
-
*/
|
|
47
|
-
function getAnthropicCredentials() {
|
|
48
|
-
const oauthToken = getEnvValue('CLAUDE_CODE_OAUTH_TOKEN');
|
|
49
|
-
if (oauthToken)
|
|
50
|
-
return { authToken: oauthToken };
|
|
51
|
-
const authToken = getEnvValue('ANTHROPIC_AUTH_TOKEN');
|
|
52
|
-
if (authToken)
|
|
53
|
-
return { authToken };
|
|
54
|
-
const apiKey = getEnvValue('ANTHROPIC_API_KEY');
|
|
55
|
-
if (apiKey)
|
|
56
|
-
return { apiKey };
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
14
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
60
15
|
function todayStr() {
|
|
61
16
|
const d = new Date();
|
|
@@ -303,24 +258,27 @@ Rules:
|
|
|
303
258
|
- Limit to at most 10 priorities, 5 cron changes, 5 new work items
|
|
304
259
|
- Focus on actionable items, not status reports
|
|
305
260
|
- If everything is on track, return minimal priorities`;
|
|
306
|
-
const creds = getAnthropicCredentials();
|
|
307
|
-
if (!creds) {
|
|
308
|
-
logger.warn('No Anthropic credentials found — generating fallback plan. Run `clementine login` to authenticate.');
|
|
309
|
-
return this.fallbackPlan(today);
|
|
310
|
-
}
|
|
311
261
|
try {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
262
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
263
|
+
let text = '';
|
|
264
|
+
const stream = query({
|
|
265
|
+
prompt,
|
|
266
|
+
options: {
|
|
267
|
+
model: MODELS.haiku,
|
|
268
|
+
maxTurns: 1,
|
|
269
|
+
systemPrompt: 'You are a planning assistant. Analyze the context and produce a prioritized daily plan as JSON. Return only valid JSON, no markdown fencing.',
|
|
270
|
+
},
|
|
318
271
|
});
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
272
|
+
for await (const msg of stream) {
|
|
273
|
+
if (msg.type === 'result')
|
|
274
|
+
text = msg.result ?? '';
|
|
275
|
+
}
|
|
276
|
+
if (!text) {
|
|
277
|
+
logger.warn('LLM returned empty plan — using fallback');
|
|
278
|
+
return this.fallbackPlan(today);
|
|
279
|
+
}
|
|
280
|
+
const cleaned = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '').trim();
|
|
281
|
+
const plan = JSON.parse(cleaned);
|
|
324
282
|
plan.date = today;
|
|
325
283
|
plan.createdAt = new Date().toISOString();
|
|
326
284
|
plan.priorities = plan.priorities ?? [];
|
|
@@ -12,54 +12,27 @@
|
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import pino from 'pino';
|
|
15
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
16
15
|
import { BASE_DIR, GOALS_DIR, MODELS } from '../config.js';
|
|
17
16
|
const logger = pino({ name: 'clementine.strategic-planner' });
|
|
18
17
|
const DAILY_PLANS_DIR = path.join(BASE_DIR, 'plans', 'daily');
|
|
19
18
|
const WEEKLY_PLANS_DIR = path.join(BASE_DIR, 'plans', 'weekly');
|
|
20
19
|
const MONTHLY_PLANS_DIR = path.join(BASE_DIR, 'plans', 'monthly');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
if (trimmed.slice(0, eqIndex) !== key)
|
|
36
|
-
continue;
|
|
37
|
-
let value = trimmed.slice(eqIndex + 1);
|
|
38
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
39
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
40
|
-
value = value.slice(1, -1);
|
|
41
|
-
}
|
|
42
|
-
return value;
|
|
20
|
+
async function llmJsonCall(prompt, systemPrompt) {
|
|
21
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
22
|
+
let text = '';
|
|
23
|
+
const stream = query({
|
|
24
|
+
prompt,
|
|
25
|
+
options: {
|
|
26
|
+
model: MODELS.haiku,
|
|
27
|
+
maxTurns: 1,
|
|
28
|
+
systemPrompt,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
for await (const msg of stream) {
|
|
32
|
+
if (msg.type === 'result')
|
|
33
|
+
text = msg.result ?? '';
|
|
43
34
|
}
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
function getAnthropicCredentials() {
|
|
47
|
-
const oauthToken = getEnvValue('CLAUDE_CODE_OAUTH_TOKEN');
|
|
48
|
-
if (oauthToken)
|
|
49
|
-
return { authToken: oauthToken };
|
|
50
|
-
const authToken = getEnvValue('ANTHROPIC_AUTH_TOKEN');
|
|
51
|
-
if (authToken)
|
|
52
|
-
return { authToken };
|
|
53
|
-
const apiKey = getEnvValue('ANTHROPIC_API_KEY');
|
|
54
|
-
if (apiKey)
|
|
55
|
-
return { apiKey };
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
function makeAnthropicClient() {
|
|
59
|
-
const creds = getAnthropicCredentials();
|
|
60
|
-
if (!creds)
|
|
61
|
-
return null;
|
|
62
|
-
return new Anthropic(creds.authToken ? { authToken: creds.authToken } : { apiKey: creds.apiKey });
|
|
35
|
+
return text;
|
|
63
36
|
}
|
|
64
37
|
// ── Strategic Planner ────────────────────────────────────────────────
|
|
65
38
|
export class StrategicPlanner {
|
|
@@ -153,16 +126,8 @@ export class StrategicPlanner {
|
|
|
153
126
|
})),
|
|
154
127
|
summary: 'No data available for weekly review.',
|
|
155
128
|
};
|
|
156
|
-
const client = makeAnthropicClient();
|
|
157
|
-
if (!client)
|
|
158
|
-
return defaultReview;
|
|
159
129
|
try {
|
|
160
|
-
const
|
|
161
|
-
model: MODELS.haiku,
|
|
162
|
-
max_tokens: 1000,
|
|
163
|
-
messages: [{ role: 'user', content: prompt }],
|
|
164
|
-
});
|
|
165
|
-
const text = response.content[0]?.type === 'text' ? response.content[0].text : '';
|
|
130
|
+
const text = await llmJsonCall(prompt, 'You synthesize weekly reviews. Return only valid JSON, no markdown fencing.');
|
|
166
131
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
167
132
|
if (jsonMatch) {
|
|
168
133
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
@@ -235,9 +200,6 @@ export class StrategicPlanner {
|
|
|
235
200
|
proposedGoals: [],
|
|
236
201
|
summary: 'No data available for monthly assessment.',
|
|
237
202
|
};
|
|
238
|
-
const client2 = makeAnthropicClient();
|
|
239
|
-
if (!client2)
|
|
240
|
-
return defaultAssessment;
|
|
241
203
|
const prompt = `You are generating a monthly strategic assessment for ${monthId}.\n\n` +
|
|
242
204
|
`${context}\n\n` +
|
|
243
205
|
`Output ONLY a JSON object:\n` +
|
|
@@ -248,12 +210,7 @@ export class StrategicPlanner {
|
|
|
248
210
|
` "summary": "2-3 sentence strategic assessment"\n` +
|
|
249
211
|
`}`;
|
|
250
212
|
try {
|
|
251
|
-
const
|
|
252
|
-
model: MODELS.haiku,
|
|
253
|
-
max_tokens: 1000,
|
|
254
|
-
messages: [{ role: 'user', content: prompt }],
|
|
255
|
-
});
|
|
256
|
-
const text = response.content[0]?.type === 'text' ? response.content[0].text : '';
|
|
213
|
+
const text = await llmJsonCall(prompt, 'You produce monthly strategic assessments. Return only valid JSON, no markdown fencing.');
|
|
257
214
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
258
215
|
if (jsonMatch) {
|
|
259
216
|
const parsed = JSON.parse(jsonMatch[0]);
|
package/dist/cli/chat.js
CHANGED
|
@@ -198,17 +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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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();
|
|
209
229
|
}
|
|
210
|
-
console.log(renderMarkdown(response));
|
|
211
|
-
console.log();
|
|
212
230
|
}
|
|
213
231
|
catch (err) {
|
|
214
232
|
process.stdout.write('\x1b[2K\r');
|
package/dist/cli/index.js
CHANGED
|
@@ -57,6 +57,13 @@ function getLaunchdPlistPath() {
|
|
|
57
57
|
const home = process.env.HOME ?? '';
|
|
58
58
|
return path.join(home, 'Library', 'LaunchAgents', `${getLaunchdLabel()}.plist`);
|
|
59
59
|
}
|
|
60
|
+
function getSystemdServiceName() {
|
|
61
|
+
return `${getAssistantName().toLowerCase()}.service`;
|
|
62
|
+
}
|
|
63
|
+
function getSystemdServicePath() {
|
|
64
|
+
const home = process.env.HOME ?? '';
|
|
65
|
+
return path.join(home, '.config', 'systemd', 'user', getSystemdServiceName());
|
|
66
|
+
}
|
|
60
67
|
function readPid() {
|
|
61
68
|
const pidFile = getPidFilePath();
|
|
62
69
|
if (!existsSync(pidFile))
|
|
@@ -104,10 +111,10 @@ function killPid(pid) {
|
|
|
104
111
|
// already dead
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
|
-
/** Stop the daemon safely:
|
|
114
|
+
/** Stop the daemon safely: disable the service manager first (prevents respawn), then kill the process. */
|
|
108
115
|
function stopDaemon(pid) {
|
|
109
|
-
// Unload LaunchAgent BEFORE killing — otherwise launchd respawns it immediately
|
|
110
116
|
if (process.platform === 'darwin') {
|
|
117
|
+
// Unload LaunchAgent BEFORE killing — otherwise launchd respawns it immediately
|
|
111
118
|
const plist = getLaunchdPlistPath();
|
|
112
119
|
if (existsSync(plist)) {
|
|
113
120
|
try {
|
|
@@ -118,6 +125,18 @@ function stopDaemon(pid) {
|
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
}
|
|
128
|
+
else if (process.platform === 'linux') {
|
|
129
|
+
// Stop systemd service BEFORE killing — otherwise systemd respawns it
|
|
130
|
+
const servicePath = getSystemdServicePath();
|
|
131
|
+
if (existsSync(servicePath)) {
|
|
132
|
+
try {
|
|
133
|
+
execSync(`systemctl --user stop ${getSystemdServiceName()}`, { stdio: 'pipe' });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// not active — that's fine
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
121
140
|
killPid(pid);
|
|
122
141
|
}
|
|
123
142
|
/** Bootstrap ~/.clementine/ on first run — create data dir and copy vault templates. */
|
|
@@ -136,43 +155,68 @@ function ensureDataHome() {
|
|
|
136
155
|
// ── Commands ─────────────────────────────────────────────────────────
|
|
137
156
|
function cmdLaunch(options) {
|
|
138
157
|
if (options.uninstall) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
if (process.platform === 'darwin') {
|
|
159
|
+
const plistPath = getLaunchdPlistPath();
|
|
160
|
+
if (existsSync(plistPath)) {
|
|
161
|
+
try {
|
|
162
|
+
execSync(`launchctl unload "${plistPath}"`, { stdio: 'ignore' });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// not loaded
|
|
166
|
+
}
|
|
167
|
+
unlinkSync(plistPath);
|
|
168
|
+
console.log(` Uninstalled LaunchAgent: ${getLaunchdLabel()}`);
|
|
143
169
|
}
|
|
144
|
-
|
|
145
|
-
|
|
170
|
+
else {
|
|
171
|
+
console.log(' LaunchAgent not installed.');
|
|
146
172
|
}
|
|
147
|
-
unlinkSync(plistPath);
|
|
148
|
-
console.log(` Uninstalled LaunchAgent: ${getLaunchdLabel()}`);
|
|
149
173
|
}
|
|
150
|
-
else {
|
|
151
|
-
|
|
174
|
+
else if (process.platform === 'linux') {
|
|
175
|
+
const servicePath = getSystemdServicePath();
|
|
176
|
+
const serviceName = getSystemdServiceName();
|
|
177
|
+
if (existsSync(servicePath)) {
|
|
178
|
+
try {
|
|
179
|
+
execSync(`systemctl --user stop ${serviceName}`, { stdio: 'ignore' });
|
|
180
|
+
execSync(`systemctl --user disable ${serviceName}`, { stdio: 'ignore' });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// not active
|
|
184
|
+
}
|
|
185
|
+
unlinkSync(servicePath);
|
|
186
|
+
try {
|
|
187
|
+
execSync('systemctl --user daemon-reload', { stdio: 'ignore' });
|
|
188
|
+
}
|
|
189
|
+
catch { /* ignore */ }
|
|
190
|
+
console.log(` Uninstalled systemd service: ${serviceName}`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
console.log(' Systemd service not installed.');
|
|
194
|
+
}
|
|
152
195
|
}
|
|
153
196
|
return;
|
|
154
197
|
}
|
|
155
198
|
if (options.install) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// Unload existing plist if already installed (idempotent reinstall)
|
|
162
|
-
if (existsSync(plistPath)) {
|
|
163
|
-
try {
|
|
164
|
-
execSync(`launchctl unload "${plistPath}"`, { stdio: 'ignore' });
|
|
199
|
+
if (process.platform === 'darwin') {
|
|
200
|
+
const plistPath = getLaunchdPlistPath();
|
|
201
|
+
const plistDir = path.dirname(plistPath);
|
|
202
|
+
if (!existsSync(plistDir)) {
|
|
203
|
+
mkdirSync(plistDir, { recursive: true });
|
|
165
204
|
}
|
|
166
|
-
|
|
167
|
-
|
|
205
|
+
// Unload existing plist if already installed (idempotent reinstall)
|
|
206
|
+
if (existsSync(plistPath)) {
|
|
207
|
+
try {
|
|
208
|
+
execSync(`launchctl unload "${plistPath}"`, { stdio: 'ignore' });
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// not loaded — fine
|
|
212
|
+
}
|
|
168
213
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
214
|
+
const nodePath = process.execPath;
|
|
215
|
+
const logDir = path.join(BASE_DIR, 'logs');
|
|
216
|
+
if (!existsSync(logDir)) {
|
|
217
|
+
mkdirSync(logDir, { recursive: true });
|
|
218
|
+
}
|
|
219
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
176
220
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
177
221
|
<plist version="1.0">
|
|
178
222
|
<dict>
|
|
@@ -204,15 +248,80 @@ function cmdLaunch(options) {
|
|
|
204
248
|
</dict>
|
|
205
249
|
</dict>
|
|
206
250
|
</plist>`;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
251
|
+
writeFileSync(plistPath, plist);
|
|
252
|
+
try {
|
|
253
|
+
execSync(`launchctl load "${plistPath}"`);
|
|
254
|
+
console.log(` Installed and loaded LaunchAgent: ${getLaunchdLabel()}`);
|
|
255
|
+
console.log(` Plist: ${plistPath}`);
|
|
256
|
+
console.log(` Logs: ${logDir}/`);
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
console.error(` Failed to load LaunchAgent: ${err}`);
|
|
260
|
+
}
|
|
213
261
|
}
|
|
214
|
-
|
|
215
|
-
|
|
262
|
+
else if (process.platform === 'linux') {
|
|
263
|
+
const servicePath = getSystemdServicePath();
|
|
264
|
+
const serviceName = getSystemdServiceName();
|
|
265
|
+
const serviceDir = path.dirname(servicePath);
|
|
266
|
+
if (!existsSync(serviceDir)) {
|
|
267
|
+
mkdirSync(serviceDir, { recursive: true });
|
|
268
|
+
}
|
|
269
|
+
// Stop existing service if already installed (idempotent reinstall)
|
|
270
|
+
if (existsSync(servicePath)) {
|
|
271
|
+
try {
|
|
272
|
+
execSync(`systemctl --user stop ${serviceName}`, { stdio: 'ignore' });
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// not active — fine
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const nodePath = process.execPath;
|
|
279
|
+
const logDir = path.join(BASE_DIR, 'logs');
|
|
280
|
+
if (!existsSync(logDir)) {
|
|
281
|
+
mkdirSync(logDir, { recursive: true });
|
|
282
|
+
}
|
|
283
|
+
const envPath = path.join(BASE_DIR, '.env');
|
|
284
|
+
const servicePATH = [path.dirname(nodePath), '/usr/local/bin', '/usr/bin', '/bin']
|
|
285
|
+
.join(':');
|
|
286
|
+
const unit = `[Unit]
|
|
287
|
+
Description=${getAssistantName()} AI Assistant
|
|
288
|
+
After=network-online.target
|
|
289
|
+
Wants=network-online.target
|
|
290
|
+
|
|
291
|
+
[Service]
|
|
292
|
+
Type=simple
|
|
293
|
+
ExecStart=${nodePath} ${DIST_ENTRY}
|
|
294
|
+
WorkingDirectory=${BASE_DIR}
|
|
295
|
+
Environment=PATH=${servicePATH}
|
|
296
|
+
Environment=CLEMENTINE_HOME=${BASE_DIR}
|
|
297
|
+
EnvironmentFile=-${envPath}
|
|
298
|
+
Restart=always
|
|
299
|
+
RestartSec=5
|
|
300
|
+
StandardOutput=append:${path.join(logDir, 'clementine.log')}
|
|
301
|
+
StandardError=append:${path.join(logDir, 'clementine-error.log')}
|
|
302
|
+
|
|
303
|
+
[Install]
|
|
304
|
+
WantedBy=default.target
|
|
305
|
+
`;
|
|
306
|
+
writeFileSync(servicePath, unit);
|
|
307
|
+
try {
|
|
308
|
+
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
|
309
|
+
execSync(`systemctl --user enable --now ${serviceName}`, { stdio: 'pipe' });
|
|
310
|
+
// Enable lingering so the service runs even when the user is not logged in (VPS)
|
|
311
|
+
try {
|
|
312
|
+
execSync(`loginctl enable-linger $(whoami)`, { stdio: 'pipe' });
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
console.log(' Note: Could not enable linger. Run as root: loginctl enable-linger $(whoami)');
|
|
316
|
+
}
|
|
317
|
+
console.log(` Installed and started systemd service: ${serviceName}`);
|
|
318
|
+
console.log(` Service: ${servicePath}`);
|
|
319
|
+
console.log(` Logs: ${logDir}/`);
|
|
320
|
+
console.log(` Status: systemctl --user status ${serviceName}`);
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
console.error(` Failed to enable systemd service: ${err}`);
|
|
324
|
+
}
|
|
216
325
|
}
|
|
217
326
|
// Also install the cron scheduler alongside the daemon
|
|
218
327
|
console.log();
|
|
@@ -681,7 +790,7 @@ function cmdDoctor(opts = {}) {
|
|
|
681
790
|
else {
|
|
682
791
|
console.log(` ${DIM} ○ Daemon not running${RESET}`);
|
|
683
792
|
}
|
|
684
|
-
//
|
|
793
|
+
// Service health check (platform-specific)
|
|
685
794
|
if (process.platform === 'darwin') {
|
|
686
795
|
const plistPath = getLaunchdPlistPath();
|
|
687
796
|
if (existsSync(plistPath)) {
|
|
@@ -700,6 +809,25 @@ function cmdDoctor(opts = {}) {
|
|
|
700
809
|
issues++;
|
|
701
810
|
}
|
|
702
811
|
}
|
|
812
|
+
else if (process.platform === 'linux') {
|
|
813
|
+
const servicePath = getSystemdServicePath();
|
|
814
|
+
const serviceName = getSystemdServiceName();
|
|
815
|
+
if (existsSync(servicePath)) {
|
|
816
|
+
try {
|
|
817
|
+
execSync(`systemctl --user is-active ${serviceName}`, { stdio: 'pipe' });
|
|
818
|
+
console.log(` ${GREEN}OK${RESET} Systemd service installed and active`);
|
|
819
|
+
}
|
|
820
|
+
catch {
|
|
821
|
+
console.log(` ${YELLOW}WARN${RESET} Systemd service installed but not active`);
|
|
822
|
+
console.log(` Start it: systemctl --user start ${serviceName}`);
|
|
823
|
+
issues++;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
console.log(` ${YELLOW}WARN${RESET} Systemd service not installed (run: clementine launch --install)`);
|
|
828
|
+
issues++;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
703
831
|
console.log();
|
|
704
832
|
if (issues === 0 && fixed === 0) {
|
|
705
833
|
console.log(` ${GREEN}All checks passed.${RESET}`);
|
|
@@ -925,6 +1053,16 @@ program
|
|
|
925
1053
|
.description('Restart the assistant (daemon by default)')
|
|
926
1054
|
.option('-f, --foreground', 'Run in foreground after restart')
|
|
927
1055
|
.action(cmdRestart);
|
|
1056
|
+
program
|
|
1057
|
+
.command('setup')
|
|
1058
|
+
.description('Run interactive setup wizard')
|
|
1059
|
+
.action(() => {
|
|
1060
|
+
ensureDataHome();
|
|
1061
|
+
runSetup().catch((err) => {
|
|
1062
|
+
console.error('Setup failed:', err);
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
928
1066
|
program
|
|
929
1067
|
.command('rebuild')
|
|
930
1068
|
.description('Rebuild from source and restart all processes (daemon + dashboard)')
|
package/dist/cli/setup.js
CHANGED
|
@@ -629,19 +629,20 @@ export async function runSetup() {
|
|
|
629
629
|
console.log(` All users: ${allowAll ? 'yes' : 'no (owner only)'}`);
|
|
630
630
|
console.log();
|
|
631
631
|
// ── Step 8: Auto-start on login ───────────────────────────────────
|
|
632
|
-
if (process.platform === 'darwin') {
|
|
632
|
+
if (process.platform === 'darwin' || process.platform === 'linux') {
|
|
633
633
|
sectionHeader('Step 8: Auto-Start');
|
|
634
|
-
|
|
635
|
-
console.log(` ${DIM}
|
|
634
|
+
const serviceName = process.platform === 'darwin' ? 'LaunchAgent' : 'systemd service';
|
|
635
|
+
console.log(` ${DIM}Install a ${serviceName} so ${entries['ASSISTANT_NAME'] || 'Clementine'} starts${RESET}`);
|
|
636
|
+
console.log(` ${DIM}automatically when the system boots.${RESET}`);
|
|
636
637
|
console.log();
|
|
637
638
|
const installService = await confirm({
|
|
638
|
-
message: 'Start automatically on
|
|
639
|
+
message: 'Start automatically on boot? (recommended)',
|
|
639
640
|
default: true,
|
|
640
641
|
});
|
|
641
642
|
if (installService) {
|
|
642
|
-
// Signal to the caller that
|
|
643
|
-
writeFileSync(path.join(BASE_DIR, '.install-
|
|
644
|
-
console.log(` ${GREEN}✔ Will install
|
|
643
|
+
// Signal to the caller that the service should be installed
|
|
644
|
+
writeFileSync(path.join(BASE_DIR, '.install-service'), '');
|
|
645
|
+
console.log(` ${GREEN}✔ Will install ${serviceName} after first launch${RESET}`);
|
|
645
646
|
}
|
|
646
647
|
}
|
|
647
648
|
console.log();
|
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;
|
|
@@ -990,6 +990,19 @@ export class CronScheduler {
|
|
|
990
990
|
}
|
|
991
991
|
catch { /* non-fatal */ }
|
|
992
992
|
}
|
|
993
|
+
// Auto-disable after too many consecutive failures — prevents zombie jobs
|
|
994
|
+
// from burning resources indefinitely. Re-enable from the dashboard.
|
|
995
|
+
if (consErrors >= 10 && !this.disabledJobs.has(job.name)) {
|
|
996
|
+
this.disabledJobs.add(job.name);
|
|
997
|
+
const scheduledTask = this.scheduledTasks.get(job.name);
|
|
998
|
+
if (scheduledTask) {
|
|
999
|
+
scheduledTask.stop();
|
|
1000
|
+
this.scheduledTasks.delete(job.name);
|
|
1001
|
+
}
|
|
1002
|
+
logger.error({ job: job.name, consErrors }, `Auto-disabled cron after ${consErrors} consecutive failures`);
|
|
1003
|
+
this.logAdvisorEvent('auto-disabled', job.name, `Auto-disabled after ${consErrors} consecutive failures`);
|
|
1004
|
+
this.dispatcher.send(`🛑 **Cron auto-disabled** — \`${job.name}\` failed ${consErrors} times in a row. Fix the job and re-enable it from the dashboard.`, { agentSlug: job.agentSlug }).catch(err => logger.debug({ err }, 'Failed to send auto-disable notification'));
|
|
1005
|
+
}
|
|
993
1006
|
}
|
|
994
1007
|
}
|
|
995
1008
|
/**
|
|
@@ -204,7 +204,7 @@ export class HeartbeatScheduler {
|
|
|
204
204
|
'(preferences, decisions, people info, project updates) to long-term memory using ' +
|
|
205
205
|
'memory_write. Skip anything already in MEMORY.md. Be selective — only save facts ' +
|
|
206
206
|
'that will be useful in future conversations. Do not create duplicate entries.', 1, // tier 1 (vault-only)
|
|
207
|
-
|
|
207
|
+
10, // max 10 turns — workflow needs read + candidates + read + write + mark_consolidated
|
|
208
208
|
'haiku').catch(err => {
|
|
209
209
|
logger.error({ err }, 'Evening memory consolidation failed');
|
|
210
210
|
});
|
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;
|