fabiana 1.1.0 → 1.2.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/dist/cli.js +9 -0
- package/dist/daemon/index.js +79 -11
- package/dist/db/init.js +35 -0
- package/dist/db/migrate-from-files.js +112 -0
- package/dist/paths.js +6 -0
- package/dist/prompts/system-consolidate.js +15 -5
- package/dist/prompts/system-solitude.js +2 -2
- package/dist/prompts/system.js +18 -8
- package/dist/setup/index.js +23 -1
- package/dist/utils/interaction.js +39 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,12 +2,15 @@ import { config as dotenvConfig } from 'dotenv';
|
|
|
2
2
|
import { paths } from './paths.js';
|
|
3
3
|
dotenvConfig({ path: paths.envFile }); // ~/.fabiana/.env (production)
|
|
4
4
|
dotenvConfig(); // .env in cwd (dev fallback)
|
|
5
|
+
import { initDb } from './db/init.js';
|
|
6
|
+
initDb();
|
|
5
7
|
import { Command } from 'commander';
|
|
6
8
|
import { readFileSync } from 'fs';
|
|
7
9
|
import { fileURLToPath } from 'url';
|
|
8
10
|
import { join, dirname } from 'path';
|
|
9
11
|
import { spawnSync } from 'child_process';
|
|
10
12
|
import { startDaemon, runInitiativeOnce, runConsolidateOnce, runSolitudeOnce } from './daemon/index.js';
|
|
13
|
+
import { runMigration } from './db/migrate-from-files.js';
|
|
11
14
|
import { runDoctor } from './doctor.js';
|
|
12
15
|
import { runBackup, runRestore } from './backup.js';
|
|
13
16
|
import { pluginsAdd, pluginsList } from './plugins-cmd.js';
|
|
@@ -165,6 +168,12 @@ model
|
|
|
165
168
|
model
|
|
166
169
|
.action(modelStatus);
|
|
167
170
|
program.addCommand(model);
|
|
171
|
+
const db = new Command('db').description('Manage the memory database');
|
|
172
|
+
db
|
|
173
|
+
.command('migrate')
|
|
174
|
+
.description('Import existing flat memory files into SQLite (run once)')
|
|
175
|
+
.action(runMigration);
|
|
176
|
+
program.addCommand(db);
|
|
168
177
|
program.addHelpCommand(new Command('help')
|
|
169
178
|
.argument('[command]', 'command to show help for')
|
|
170
179
|
.description('Show help for fabiana or a specific command'));
|
package/dist/daemon/index.js
CHANGED
|
@@ -8,7 +8,8 @@ import { PermissionValidator } from '../utils/permissions.js';
|
|
|
8
8
|
import { Logger } from '../utils/logger.js';
|
|
9
9
|
import { createFabianaTools } from '../tools/index.js';
|
|
10
10
|
import { loadContext, buildPrompt } from '../loaders/context.js';
|
|
11
|
-
import { loadMood,
|
|
11
|
+
import { loadMood, selectInitiativeType } from '../initiative/trigger.js';
|
|
12
|
+
import { updateLastInteraction, updateLastSolitude, getLastInteractionHoursAgo, getLastSolitudeHoursAgo } from '../utils/interaction.js';
|
|
12
13
|
import { ALL_TYPES, TYPE_INSTRUCTIONS } from '../initiative/types.js';
|
|
13
14
|
import { ALL_SOLITUDE_TYPES, SOLITUDE_TYPE_INSTRUCTIONS } from '../solitude/types.js';
|
|
14
15
|
import { loadPlugins } from '../loaders/plugins.js';
|
|
@@ -86,6 +87,13 @@ export async function runPiSession(mode, incomingMessage, channel, incomingMsg,
|
|
|
86
87
|
if (conversationState && conversationManager) {
|
|
87
88
|
await conversationManager.append(conversationState.id, 'fabiana', text);
|
|
88
89
|
}
|
|
90
|
+
// Track all outbound messages as interactions (resets idle clock)
|
|
91
|
+
if (mode !== 'solitude') {
|
|
92
|
+
await updateLastInteraction();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
await updateLastSolitude();
|
|
96
|
+
}
|
|
89
97
|
};
|
|
90
98
|
console.log('[6/8] Creating tools...');
|
|
91
99
|
const fabianaTools = createFabianaTools(permissions, sendMessage, {
|
|
@@ -338,24 +346,46 @@ export async function startDaemon() {
|
|
|
338
346
|
await ch.start();
|
|
339
347
|
}
|
|
340
348
|
await sendStartupMessage(primaryChannel);
|
|
349
|
+
/**
|
|
350
|
+
* Schedules a recurring task with ±jitterMinutes random variation per invocation.
|
|
351
|
+
* This makes Fabiana's self-triggered behaviours feel less mechanical.
|
|
352
|
+
*/
|
|
353
|
+
function scheduleWithJitter(intervalMinutes, jitterMinutes, handler) {
|
|
354
|
+
const schedule = () => {
|
|
355
|
+
const jitter = (Math.random() * 2 - 1) * jitterMinutes * 60_000; // ±jitter in ms
|
|
356
|
+
const delay = intervalMinutes * 60_000 + jitter;
|
|
357
|
+
setTimeout(async () => {
|
|
358
|
+
try {
|
|
359
|
+
await handler();
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
console.error('❌ [JITTER-SCHEDULER] Uncaught error:', err.message);
|
|
363
|
+
}
|
|
364
|
+
schedule(); // re-schedule after each run
|
|
365
|
+
}, delay);
|
|
366
|
+
};
|
|
367
|
+
schedule();
|
|
368
|
+
}
|
|
341
369
|
const initiative = config.initiative;
|
|
342
370
|
if (initiative.enabled) {
|
|
343
|
-
const intervalMinutes = initiative.checkIntervalMinutes ??
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
: `*/${intervalMinutes} * * * *`;
|
|
347
|
-
console.log(`[INIT] Initiative checks every ${intervalMinutes}min (active ${initiative.activeHoursStart}:00–${initiative.activeHoursEnd}:00)`);
|
|
348
|
-
cron.schedule(cronExpr, async () => {
|
|
371
|
+
const intervalMinutes = initiative.checkIntervalMinutes ?? 30;
|
|
372
|
+
console.log(`[INIT] Initiative checks every ~${intervalMinutes}min ±15min (active ${initiative.activeHoursStart}:00–${initiative.activeHoursEnd}:00)`);
|
|
373
|
+
scheduleWithJitter(intervalMinutes, 15, async () => {
|
|
349
374
|
const hour = new Date().getHours();
|
|
350
375
|
if (hour < initiative.activeHoursStart || hour >= initiative.activeHoursEnd) {
|
|
351
376
|
console.log(`\n🌱 [SCHEDULED] Initiative skipped — outside active hours (${hour}:00)`);
|
|
352
377
|
return;
|
|
353
378
|
}
|
|
379
|
+
// Gate: skip if last interaction was too recent
|
|
380
|
+
const hoursSinceInteraction = await getLastInteractionHoursAgo();
|
|
381
|
+
if (hoursSinceInteraction !== null && hoursSinceInteraction < initiative.minHoursBetweenMessages) {
|
|
382
|
+
console.log(`\n🌱 [SCHEDULED] Initiative skipped — last interaction ${hoursSinceInteraction.toFixed(1)}h ago (min: ${initiative.minHoursBetweenMessages}h)`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
354
385
|
console.log('\n🌱 [SCHEDULED] Running initiative check...');
|
|
355
386
|
try {
|
|
356
387
|
const mood = await loadMood();
|
|
357
|
-
const
|
|
358
|
-
const { type: selectedType, reason } = selectInitiativeType(mood, hoursSince);
|
|
388
|
+
const { type: selectedType, reason } = selectInitiativeType(mood, hoursSinceInteraction);
|
|
359
389
|
const typeInstruction = TYPE_INSTRUCTIONS[selectedType];
|
|
360
390
|
console.log(` 🎯 Type: ${selectedType} (${reason})`);
|
|
361
391
|
await runPiSession('initiative', undefined, primaryChannel, undefined, undefined, channels, conversationManager, { type: selectedType, typeInstruction });
|
|
@@ -366,6 +396,43 @@ export async function startDaemon() {
|
|
|
366
396
|
}
|
|
367
397
|
});
|
|
368
398
|
}
|
|
399
|
+
const solitude = config.solitude;
|
|
400
|
+
if (solitude?.enabled) {
|
|
401
|
+
const solitudeInterval = solitude.checkIntervalMinutes ?? 30;
|
|
402
|
+
const minIdleHours = solitude.minIdleHours ?? 1;
|
|
403
|
+
const minCooldownHours = solitude.minCooldownHours ?? 2;
|
|
404
|
+
console.log(`[INIT] Solitude checks every ~${solitudeInterval}min ±15min (idle>${minIdleHours}h, cooldown>${minCooldownHours}h, active ${solitude.activeHoursStart}:00–${solitude.activeHoursEnd}:00)`);
|
|
405
|
+
scheduleWithJitter(solitudeInterval, 15, async () => {
|
|
406
|
+
const hour = new Date().getHours();
|
|
407
|
+
if (hour < (solitude.activeHoursStart ?? 7) || hour >= (solitude.activeHoursEnd ?? 23)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const hoursSinceInteraction = await getLastInteractionHoursAgo();
|
|
411
|
+
const hoursSinceSolitude = await getLastSolitudeHoursAgo();
|
|
412
|
+
// Only enter solitude if idle long enough
|
|
413
|
+
if (hoursSinceInteraction !== null && hoursSinceInteraction < minIdleHours) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
// Respect cooldown between solitude sessions
|
|
417
|
+
if (hoursSinceSolitude !== null && hoursSinceSolitude < minCooldownHours) {
|
|
418
|
+
console.log(`\n🌿 [SCHEDULED] Solitude skipped — last solitude ${hoursSinceSolitude.toFixed(1)}h ago (cooldown: ${minCooldownHours}h)`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
console.log('\n🌿 [SCHEDULED] Entering solitude...');
|
|
422
|
+
try {
|
|
423
|
+
const selectedType = ALL_SOLITUDE_TYPES[Math.floor(Math.random() * ALL_SOLITUDE_TYPES.length)];
|
|
424
|
+
const typeInstruction = SOLITUDE_TYPE_INSTRUCTIONS[selectedType];
|
|
425
|
+
console.log(` 🌿 Type: ${selectedType}`);
|
|
426
|
+
await runPiSession('solitude', undefined, primaryChannel, undefined, undefined, channels, conversationManager, undefined, { type: selectedType, typeInstruction });
|
|
427
|
+
// Always update lastSolitude after a session, even if no message was sent
|
|
428
|
+
await updateLastSolitude();
|
|
429
|
+
console.log('✅ [SCHEDULED] Solitude complete');
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
console.error('❌ [SCHEDULED] Solitude failed:', err.message);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
369
436
|
cron.schedule('0 0 * * *', async () => {
|
|
370
437
|
console.log('\n🌙 [SCHEDULED] Running midnight consolidation...');
|
|
371
438
|
try {
|
|
@@ -429,6 +496,7 @@ export async function startDaemon() {
|
|
|
429
496
|
// Owner message — full chat session on the channel it arrived on
|
|
430
497
|
console.log(`\n📨 [${msg.source}] Message: "${msg.text.slice(0, 50)}..."`);
|
|
431
498
|
await msgChannel.logConversation('user', msg.text, msg.source);
|
|
499
|
+
await updateLastInteraction();
|
|
432
500
|
try {
|
|
433
501
|
await runPiSession('chat', msg.text, msgChannel, msg, undefined, channels, conversationManager);
|
|
434
502
|
}
|
|
@@ -457,7 +525,7 @@ export async function runInitiativeOnce(forcedType, dryRun = false) {
|
|
|
457
525
|
console.log('━'.repeat(50));
|
|
458
526
|
// Resolve initiative type — forced via CLI or auto-selected by trigger engine
|
|
459
527
|
const mood = await loadMood();
|
|
460
|
-
const hoursSince = await
|
|
528
|
+
const hoursSince = await getLastInteractionHoursAgo();
|
|
461
529
|
let selectedType;
|
|
462
530
|
let selectionReason;
|
|
463
531
|
if (forcedType) {
|
|
@@ -480,7 +548,7 @@ export async function runInitiativeOnce(forcedType, dryRun = false) {
|
|
|
480
548
|
console.log(`💭 Mood: ${mood.current} (intensity: ${mood.intensity.toFixed(2)})`);
|
|
481
549
|
}
|
|
482
550
|
if (hoursSince !== null) {
|
|
483
|
-
console.log(`⏱️ Hours since last
|
|
551
|
+
console.log(`⏱️ Hours since last interaction: ${hoursSince.toFixed(1)}h`);
|
|
484
552
|
}
|
|
485
553
|
if (dryRun) {
|
|
486
554
|
console.log('\n── Dry run — not sending ─────────────────────────────');
|
package/dist/db/init.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { paths } from '../paths.js';
|
|
6
|
+
function schemaPath() {
|
|
7
|
+
// Works in both dev (src/db/init.ts) and prod (dist/db/init.js)
|
|
8
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
return join(__dir, 'schema.sql');
|
|
10
|
+
}
|
|
11
|
+
export function initDb() {
|
|
12
|
+
// Check sqlite3 is available
|
|
13
|
+
try {
|
|
14
|
+
execSync('sqlite3 --version', { stdio: 'ignore' });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
console.warn('[memory-db] sqlite3 not found — structured memory will not be available.\n' +
|
|
18
|
+
' Install with: sudo apt install sqlite3 (Linux) or brew install sqlite3 (Mac)');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (existsSync(paths.memoryDb))
|
|
22
|
+
return;
|
|
23
|
+
const schema = schemaPath();
|
|
24
|
+
if (!existsSync(schema)) {
|
|
25
|
+
console.warn(`[memory-db] Schema file not found at ${schema} — skipping DB init`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
execSync(`sqlite3 "${paths.memoryDb}" < "${schema}"`);
|
|
30
|
+
console.log(`[memory-db] Initialized ${paths.memoryDb}`);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.warn(`[memory-db] Failed to initialize DB: ${err.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { join, basename } from 'path';
|
|
4
|
+
import { paths } from '../paths.js';
|
|
5
|
+
function sqlEscape(s) {
|
|
6
|
+
return s.replace(/'/g, "''");
|
|
7
|
+
}
|
|
8
|
+
function runSql(sql) {
|
|
9
|
+
execSync(`sqlite3 "${paths.memoryDb}"`, { input: sql, stdio: ['pipe', 'inherit', 'inherit'] });
|
|
10
|
+
}
|
|
11
|
+
function capitalizeName(filename) {
|
|
12
|
+
return filename
|
|
13
|
+
.split(/[-_\s]+/)
|
|
14
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
15
|
+
.join(' ');
|
|
16
|
+
}
|
|
17
|
+
export async function runMigration() {
|
|
18
|
+
try {
|
|
19
|
+
execSync('sqlite3 --version', { stdio: 'ignore' });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
console.error('❌ sqlite3 not found. Install with: sudo apt install sqlite3 (Linux) or brew install sqlite3 (Mac)');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (!existsSync(paths.memoryDb)) {
|
|
26
|
+
console.error(`❌ memory.db not found at ${paths.memoryDb}. Run \`fabiana init\` first.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const memoryRoot = paths.memory();
|
|
30
|
+
let peopleCount = 0;
|
|
31
|
+
let interestCount = 0;
|
|
32
|
+
let factCount = 0;
|
|
33
|
+
console.log('\n🗄️ Migrating flat memory files → SQLite\n');
|
|
34
|
+
// ── 1. People ────────────────────────────────────────────────────────────
|
|
35
|
+
const peopleDir = join(memoryRoot, 'people');
|
|
36
|
+
if (existsSync(peopleDir)) {
|
|
37
|
+
const files = readdirSync(peopleDir).filter(f => f.endsWith('.md'));
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const raw = basename(file, '.md');
|
|
40
|
+
const name = capitalizeName(raw);
|
|
41
|
+
const bio = readFileSync(join(peopleDir, file), 'utf-8').trim();
|
|
42
|
+
// Best-effort relationship guess from the file content
|
|
43
|
+
let relationship = null;
|
|
44
|
+
const lowerBio = bio.toLowerCase();
|
|
45
|
+
if (/\bfamily\b|\bmother\b|\bfather\b|\bsister\b|\bbrother\b|\bwife\b|\bhusband\b|\bchild\b|\bchildren\b/.test(lowerBio)) {
|
|
46
|
+
relationship = 'family';
|
|
47
|
+
}
|
|
48
|
+
else if (/\bfriend\b/.test(lowerBio)) {
|
|
49
|
+
relationship = 'friend';
|
|
50
|
+
}
|
|
51
|
+
else if (/\bcolleague\b|\bworks\b|\bteam\b|\bmanager\b|\breport\b|\bco-worker\b/.test(lowerBio)) {
|
|
52
|
+
relationship = 'colleague';
|
|
53
|
+
}
|
|
54
|
+
const relVal = relationship ? `'${relationship}'` : 'NULL';
|
|
55
|
+
runSql(`INSERT INTO people (name, relationship, bio) VALUES ('${sqlEscape(name)}', ${relVal}, '${sqlEscape(bio)}') ON CONFLICT(name) DO UPDATE SET bio = excluded.bio, relationship = COALESCE(excluded.relationship, relationship);`);
|
|
56
|
+
console.log(` 👤 ${name}`);
|
|
57
|
+
peopleCount++;
|
|
58
|
+
}
|
|
59
|
+
console.log(`✓ People: ${peopleCount} imported\n`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.log(' People: no people/ dir — skipping\n');
|
|
63
|
+
}
|
|
64
|
+
// ── 2. Interests ─────────────────────────────────────────────────────────
|
|
65
|
+
const topicsPath = join(memoryRoot, 'interests', 'topics.md');
|
|
66
|
+
if (existsSync(topicsPath)) {
|
|
67
|
+
const content = readFileSync(topicsPath, 'utf-8');
|
|
68
|
+
let currentSection = 'general';
|
|
69
|
+
for (const line of content.split('\n')) {
|
|
70
|
+
if (line.startsWith('## ')) {
|
|
71
|
+
currentSection = line.replace(/^## /, '').trim().toLowerCase().replace(/\s+/g, '_');
|
|
72
|
+
}
|
|
73
|
+
else if (line.startsWith('- ')) {
|
|
74
|
+
const topic = line.replace(/^- /, '').trim();
|
|
75
|
+
if (!topic)
|
|
76
|
+
continue;
|
|
77
|
+
runSql(`INSERT OR IGNORE INTO memories (content, category, subject, tier) VALUES ('${sqlEscape(topic)}', 'interest', '${sqlEscape(currentSection)}', 'warm');`);
|
|
78
|
+
interestCount++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
console.log(`✓ Interests: ${interestCount} imported\n`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log(' Interests: no topics.md — skipping\n');
|
|
85
|
+
}
|
|
86
|
+
// ── 3. This week ─────────────────────────────────────────────────────────
|
|
87
|
+
const thisWeekPath = join(memoryRoot, 'recent', 'this-week.md');
|
|
88
|
+
if (existsSync(thisWeekPath)) {
|
|
89
|
+
const content = readFileSync(thisWeekPath, 'utf-8').trim();
|
|
90
|
+
if (content) {
|
|
91
|
+
runSql(`INSERT OR IGNORE INTO memories (content, category, subject, tier) VALUES ('${sqlEscape(content)}', 'fact', 'this_week', 'hot');`);
|
|
92
|
+
factCount++;
|
|
93
|
+
console.log(`✓ This-week: imported\n`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ── 4. Upcoming dates ────────────────────────────────────────────────────
|
|
97
|
+
// The format is too variable for structured parsing — import as a single fact row.
|
|
98
|
+
// Fabiana can use the memory-db skill to normalise these into proper events rows herself.
|
|
99
|
+
const upcomingPath = join(memoryRoot, 'dates', 'upcoming.md');
|
|
100
|
+
if (existsSync(upcomingPath)) {
|
|
101
|
+
const content = readFileSync(upcomingPath, 'utf-8').trim();
|
|
102
|
+
if (content) {
|
|
103
|
+
runSql(`INSERT OR IGNORE INTO memories (content, category, subject, tier) VALUES ('${sqlEscape(content)}', 'fact', 'upcoming_dates', 'warm');`);
|
|
104
|
+
factCount++;
|
|
105
|
+
console.log(`✓ Upcoming dates: imported as fact row (let Fabiana normalise into events table)\n`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.log('━'.repeat(50));
|
|
109
|
+
console.log(`✅ Migration complete`);
|
|
110
|
+
console.log(` People: ${peopleCount} | Interests: ${interestCount} | Facts: ${factCount}`);
|
|
111
|
+
console.log(`\n The flat .md files are unchanged — DB is the canonical store going forward.`);
|
|
112
|
+
}
|
package/dist/paths.js
CHANGED
|
@@ -20,6 +20,8 @@ export const paths = {
|
|
|
20
20
|
agentTodo: join(DATA_DIR, 'agent-todo'),
|
|
21
21
|
conversations: join(DATA_DIR, 'conversations'),
|
|
22
22
|
moodMd: join(DATA_DIR, 'memory', 'mood.md'),
|
|
23
|
+
memoryDb: join(DATA_DIR, 'memory.db'),
|
|
24
|
+
lastInteractionJson: join(DATA_DIR, 'last_interaction.json'),
|
|
23
25
|
envFile: join(FABIANA_HOME, '.env'),
|
|
24
26
|
};
|
|
25
27
|
// Default plugins bundled with the package.
|
|
@@ -29,5 +31,9 @@ const __dir = fileURLToPath(new URL('.', import.meta.url));
|
|
|
29
31
|
const distPlugins = join(__dir, 'plugins');
|
|
30
32
|
const srcPlugins = join(__dir, '..', 'plugins');
|
|
31
33
|
export const BUNDLED_PLUGINS_DIR = existsSync(distPlugins) ? distPlugins : srcPlugins;
|
|
34
|
+
// Default skills bundled with the package.
|
|
35
|
+
// Dev: src/paths.ts → __dir = src/ → src/skills/
|
|
36
|
+
// Prod: dist/paths.js → __dir = dist/ → dist/skills/
|
|
37
|
+
export const BUNDLED_SKILLS_DIR = join(__dir, 'skills');
|
|
32
38
|
// Root of the installed/dev package (one level above src/ or dist/)
|
|
33
39
|
export const PACKAGE_ROOT = join(__dir, '..');
|
|
@@ -24,17 +24,27 @@ Create: \`data/memory/diary/YYYY/YYYY-MM/YYYY-MM-DD.md\`
|
|
|
24
24
|
|
|
25
25
|
Keep it human and readable — like a journal entry written by someone who cares, not a bullet report.
|
|
26
26
|
|
|
27
|
-
### 4.
|
|
27
|
+
### 4. Write to Structured Memory (SQLite)
|
|
28
|
+
|
|
29
|
+
Use the **memory-db** skill to persist what you extracted:
|
|
30
|
+
|
|
31
|
+
- Any new or updated person → upsert into \`people\` (name, relationship, bio)
|
|
32
|
+
- Any upcoming date or commitment → INSERT into \`events\` (title, date YYYY-MM-DD, notes)
|
|
33
|
+
- Today's mood assessment of {{user_name}} → INSERT into \`moods\` (value, intensity 1–10, note)
|
|
34
|
+
- Notable recurring topics or facts → INSERT into \`memories\` (category=\`'fact'\`, content, subject)
|
|
35
|
+
- Archive cold memories: \`UPDATE memories SET expires_at = datetime('now') WHERE tier = 'cold' AND created_at < datetime('now', '-90 days') AND expires_at IS NULL;\`
|
|
36
|
+
|
|
37
|
+
### 5. Update Memory Files
|
|
38
|
+
|
|
39
|
+
Keep the hot-tier files current (they load into every session):
|
|
28
40
|
- New person info → \`data/memory/people/[name].md\` (create if doesn't exist)
|
|
29
|
-
- Upcoming events → \`data/memory/dates/upcoming.md\`
|
|
30
|
-
- New interests/topics → \`data/memory/interests/topics.md\`
|
|
31
41
|
- Health updates → \`data/memory/health.md\`
|
|
32
42
|
|
|
33
|
-
###
|
|
43
|
+
### 6. Update Rolling Memory
|
|
34
44
|
- \`data/memory/recent/this-week.md\` — append today's one-paragraph summary (keep last 7 days, trim older)
|
|
35
45
|
- \`data/memory/core.md\` — update current state, active threads, last_message_sent if applicable
|
|
36
46
|
|
|
37
|
-
###
|
|
47
|
+
### 7. Clean Up TODOs
|
|
38
48
|
- \`manage_todo action=list\` — review all pending
|
|
39
49
|
- Mark completed ones done
|
|
40
50
|
- Delete stale ones that are no longer relevant
|
|
@@ -26,7 +26,7 @@ No one messaged you. This is your time. The system has given you a **solitude ty
|
|
|
26
26
|
## Hard Rules
|
|
27
27
|
|
|
28
28
|
- Only call \`send_message\` if you genuinely found something time-sensitive or too good not to share — *not* as a habit
|
|
29
|
-
- Write
|
|
30
|
-
-
|
|
29
|
+
- Write long-form prose (diary entries, reflections, creative writing) to \`data/memory/self/\` — it belongs to you
|
|
30
|
+
- For structured insights (people, events, facts, news items, mood), use the **memory-db** skill — not markdown files
|
|
31
31
|
- Your full work is logged regardless — you don't need to summarize it for anyone
|
|
32
32
|
`;
|
package/dist/prompts/system.js
CHANGED
|
@@ -48,14 +48,24 @@ At session start, your context loader injects core memory into the user prompt:
|
|
|
48
48
|
Proactively pull additional memory files when relevant using \`safe_read\`.
|
|
49
49
|
|
|
50
50
|
### Write Protocol
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
|
|
52
|
+
**Structured data → memory-db skill (preferred)**
|
|
53
|
+
|
|
54
|
+
Use the \`memory-db\` skill for anything searchable or relational:
|
|
55
|
+
- New info about a person → \`people\` table (upsert by name)
|
|
56
|
+
- Upcoming date or event → \`events\` table
|
|
57
|
+
- Discrete facts about {{user_name}} → \`memories\` table (\`category='fact'\`)
|
|
58
|
+
- Interests and research → \`memories\` table (\`category='interest'\`)
|
|
59
|
+
- Mood at end of session → \`moods\` table
|
|
60
|
+
- News worth noting → \`memories\` table (\`category='news'\`)
|
|
61
|
+
|
|
62
|
+
**Hot-tier files (always update directly)**
|
|
63
|
+
|
|
64
|
+
Keep these files current — they are loaded into every session:
|
|
65
|
+
- Identity anchor facts → \`data/memory/identity.md\`
|
|
66
|
+
- Current state / active threads → \`data/memory/core.md\`
|
|
67
|
+
|
|
68
|
+
**Format for file updates:**
|
|
59
69
|
\`\`\`
|
|
60
70
|
- [YYYY-MM-DD] [fact]
|
|
61
71
|
\`\`\`
|
package/dist/setup/index.js
CHANGED
|
@@ -3,12 +3,13 @@ import chalk from 'chalk';
|
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { providers } from '../data/providers.js';
|
|
6
|
-
import { paths, PLUGINS_DIR, CONFIG_DIR, DATA_DIR, BUNDLED_PLUGINS_DIR, PACKAGE_ROOT } from '../paths.js';
|
|
6
|
+
import { paths, PLUGINS_DIR, SKILLS_DIR, CONFIG_DIR, DATA_DIR, BUNDLED_PLUGINS_DIR, BUNDLED_SKILLS_DIR, PACKAGE_ROOT } from '../paths.js';
|
|
7
7
|
import { systemPromptTemplate } from '../prompts/system.js';
|
|
8
8
|
import { systemChatTemplate } from '../prompts/system-chat.js';
|
|
9
9
|
import { systemInitiativeTemplate } from '../prompts/system-initiative.js';
|
|
10
10
|
import { systemConsolidateTemplate } from '../prompts/system-consolidate.js';
|
|
11
11
|
import { systemExternalTemplate } from '../prompts/system-external.js';
|
|
12
|
+
import { initDb } from '../db/init.js';
|
|
12
13
|
const TONES = {
|
|
13
14
|
'warm-casual': {
|
|
14
15
|
label: 'Warm & Casual',
|
|
@@ -218,6 +219,7 @@ export async function runSetup() {
|
|
|
218
219
|
await fs.mkdir(path.join(DATA_DIR, 'logs'), { recursive: true });
|
|
219
220
|
await fs.mkdir(path.join(DATA_DIR, 'sessions'), { recursive: true });
|
|
220
221
|
await fs.mkdir(PLUGINS_DIR, { recursive: true });
|
|
222
|
+
await fs.mkdir(SKILLS_DIR, { recursive: true });
|
|
221
223
|
// config
|
|
222
224
|
const config = {
|
|
223
225
|
version: '0.1.0',
|
|
@@ -312,6 +314,26 @@ You can read those files to understand your own capabilities and limitations.
|
|
|
312
314
|
catch {
|
|
313
315
|
// No bundled plugins dir — dev environment or stripped build, skip silently
|
|
314
316
|
}
|
|
317
|
+
// copy bundled default skills (skip any already installed)
|
|
318
|
+
try {
|
|
319
|
+
const bundledSkillDirs = await fs.readdir(BUNDLED_SKILLS_DIR, { withFileTypes: true });
|
|
320
|
+
for (const entry of bundledSkillDirs.filter(e => e.isDirectory())) {
|
|
321
|
+
const dest = path.join(SKILLS_DIR, entry.name);
|
|
322
|
+
try {
|
|
323
|
+
await fs.access(dest);
|
|
324
|
+
// already exists — skip so user customisations are preserved
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
await fs.cp(path.join(BUNDLED_SKILLS_DIR, entry.name), dest, { recursive: true });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
ok(`${SKILLS_DIR}/ (default skills copied)`);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// No bundled skills dir — dev environment or stripped build, skip silently
|
|
334
|
+
}
|
|
335
|
+
// initialise SQLite memory DB
|
|
336
|
+
initDb();
|
|
315
337
|
// ── API key setup ─────────────────────────────────────────────
|
|
316
338
|
const envPath = paths.envFile;
|
|
317
339
|
const shell = process.env.SHELL ?? '';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { paths } from '../paths.js';
|
|
3
|
+
async function readState() {
|
|
4
|
+
try {
|
|
5
|
+
const content = await fs.readFile(paths.lastInteractionJson, 'utf-8');
|
|
6
|
+
return JSON.parse(content);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return { lastInteraction: null, lastSolitude: null };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function writeState(state) {
|
|
13
|
+
await fs.mkdir(paths.lastInteractionJson.replace(/\/[^/]+$/, ''), { recursive: true });
|
|
14
|
+
await fs.writeFile(paths.lastInteractionJson, JSON.stringify(state, null, 2), 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
export async function updateLastInteraction() {
|
|
17
|
+
const state = await readState();
|
|
18
|
+
state.lastInteraction = new Date().toISOString();
|
|
19
|
+
await writeState(state);
|
|
20
|
+
}
|
|
21
|
+
export async function updateLastSolitude() {
|
|
22
|
+
const state = await readState();
|
|
23
|
+
state.lastSolitude = new Date().toISOString();
|
|
24
|
+
await writeState(state);
|
|
25
|
+
}
|
|
26
|
+
export async function getLastInteractionHoursAgo() {
|
|
27
|
+
const state = await readState();
|
|
28
|
+
if (!state.lastInteraction)
|
|
29
|
+
return null;
|
|
30
|
+
const ms = Date.now() - new Date(state.lastInteraction).getTime();
|
|
31
|
+
return ms / 3_600_000;
|
|
32
|
+
}
|
|
33
|
+
export async function getLastSolitudeHoursAgo() {
|
|
34
|
+
const state = await readState();
|
|
35
|
+
if (!state.lastSolitude)
|
|
36
|
+
return null;
|
|
37
|
+
const ms = Date.now() - new Date(state.lastSolitude).getTime();
|
|
38
|
+
return ms / 3_600_000;
|
|
39
|
+
}
|