Add daily journal system (Milestone 5)

- Add journal service with date-based organization
- Add quick capture with timestamps and tags
- Add auto-linking for mentioned nodes and hashtags
- Add AI-powered daily summary generation
- Add journal search across all entries
- Add CLI commands: journal, j (alias), c (quick capture)
- Add MCP tools: memory_journal, memory_journal_list, memory_journal_summarize
This commit is contained in:
2026-02-03 10:57:23 +01:00
parent 056a02d936
commit 67b1e3b481
4 changed files with 446 additions and 0 deletions

160
src/cli/commands/journal.ts Normal file
View File

@@ -0,0 +1,160 @@
import { Command } from 'commander';
import chalk from 'chalk';
import {
getOrCreateJournal,
appendToJournal,
listJournals,
searchJournals,
generateJournalSummary,
getJournalByDate,
formatDate,
getDateFromOffset,
JournalMetadata,
} from '../../core/journal';
export const journalCommand = new Command('journal')
.description('Daily journal - quick capture and daily notes')
.argument('[text...]', 'Text to add to today\'s journal')
.option('-d, --date <date>', 'Specific date (YYYY-MM-DD)')
.option('--yesterday', 'Yesterday\'s journal')
.option('-l, --list', 'List recent journals')
.option('--month <month>', 'Filter by month (YYYY-MM)')
.option('-s, --search <query>', 'Search journals')
.option('--summarize', 'Generate AI summary')
.option('-t, --tags <tags>', 'Tags for the entry (comma-separated)')
.action(async (textParts: string[], opts) => {
try {
// Determine target date
let targetDate: string | undefined;
if (opts.date) {
targetDate = opts.date;
} else if (opts.yesterday) {
targetDate = getDateFromOffset('yesterday');
}
// List journals
if (opts.list) {
const journals = listJournals({ month: opts.month, limit: 20 });
if (journals.length === 0) {
console.log(chalk.yellow('No journals found.'));
return;
}
console.log(chalk.cyan('Recent Journals:\n'));
for (const j of journals) {
const meta = j.metadata as JournalMetadata;
const entryCount = meta.entries?.length || 0;
const summaryIndicator = meta.summary ? ' [summarized]' : '';
console.log(` ${chalk.white(meta.date)} ${chalk.dim(`${entryCount} entries`)}${chalk.green(summaryIndicator)}`);
}
return;
}
// Search journals
if (opts.search) {
const results = await searchJournals(opts.search);
if (results.length === 0) {
console.log(chalk.yellow('No matching journals found.'));
return;
}
console.log(chalk.cyan(`Found ${results.length} journals:\n`));
for (const j of results) {
const meta = j.metadata as JournalMetadata;
console.log(chalk.white(`${meta.date}:`));
// Show matching entries
const entries = meta.entries?.filter(e =>
e.text.toLowerCase().includes(opts.search.toLowerCase())
) || [];
for (const e of entries.slice(0, 3)) {
console.log(chalk.dim(` ${e.time}`) + ` ${e.text}`);
}
if (entries.length > 3) {
console.log(chalk.dim(` ... and ${entries.length - 3} more`));
}
console.log();
}
return;
}
// Generate summary
if (opts.summarize) {
console.log(chalk.cyan('Generating summary...'));
const summary = await generateJournalSummary(targetDate);
if (!summary) {
console.log(chalk.yellow('No entries to summarize.'));
return;
}
console.log();
console.log(chalk.green('Summary:'));
console.log(summary);
return;
}
// Add entry or view journal
const text = textParts.join(' ').trim();
if (text) {
// Add entry
const tags = opts.tags?.split(',').map((t: string) => t.trim());
const { journal, entry } = await appendToJournal(text, { tags, date: targetDate });
const meta = journal.metadata as JournalMetadata;
console.log(chalk.green(`✓ Added to ${meta.date}`));
console.log(chalk.dim(` ${entry.time}`) + ` ${text}`);
} else {
// View journal
const journal = await getOrCreateJournal(targetDate);
const meta = journal.metadata as JournalMetadata;
console.log(chalk.cyan(`Journal: ${meta.date}`));
console.log(chalk.dim(`ID: ${journal.id.slice(0, 8)}`));
console.log();
if (!meta.entries?.length) {
console.log(chalk.dim('No entries yet. Add one with: cortex journal "your text"'));
} else {
for (const entry of meta.entries) {
const tagStr = entry.tags?.length ? chalk.blue(` [${entry.tags.join(', ')}]`) : '';
console.log(chalk.dim(`${entry.time}`) + tagStr + ` ${entry.text}`);
}
}
if (meta.summary) {
console.log();
console.log(chalk.green('Summary:'));
console.log(meta.summary);
}
}
} catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`));
process.exit(1);
}
});
// Alias: cortex j
export const journalAliasCommand = new Command('j')
.description('Alias for journal command')
.argument('[text...]', 'Text to add')
.option('-t, --tags <tags>', 'Tags (comma-separated)')
.action(async (textParts: string[], opts) => {
const text = textParts.join(' ').trim();
if (!text) {
const journal = await getOrCreateJournal();
const meta = journal.metadata as JournalMetadata;
console.log(chalk.cyan(`Journal: ${meta.date} (${meta.entries?.length || 0} entries)`));
return;
}
const tags = opts.tags?.split(',').map((t: string) => t.trim());
const { journal, entry } = await appendToJournal(text, { tags });
const meta = journal.metadata as JournalMetadata;
console.log(chalk.green(`${meta.date} ${entry.time}`) + ` ${text}`);
});
// Quick capture: cortex c
export const quickCaptureCommand = new Command('c')
.description('Quick capture to today\'s journal')
.argument('<text...>', 'Text to capture')
.option('-t, --tags <tags>', 'Tags (comma-separated)')
.action(async (textParts: string[], opts) => {
const text = textParts.join(' ').trim();
const tags = opts.tags?.split(',').map((t: string) => t.trim());
const { entry } = await appendToJournal(text, { tags });
console.log(chalk.green(`${entry.time}`) + ` ${text}`);
});

View File

@@ -17,6 +17,7 @@ import { restoreCommand } from './commands/restore';
import { captureCommand, captureHookCommand, configCommand } from './commands/capture';
import { contextCommand, contextHookCommand } from './commands/context';
import { indexCommand } from './commands/index-cmd';
import { journalCommand, journalAliasCommand, quickCaptureCommand } from './commands/journal';
import { closeDb } from '../core/db';
const program = new Command();
@@ -46,6 +47,9 @@ program.addCommand(contextCommand);
program.addCommand(contextHookCommand);
program.addCommand(configCommand);
program.addCommand(indexCommand);
program.addCommand(journalCommand);
program.addCommand(journalAliasCommand);
program.addCommand(quickCaptureCommand);
program.hook('postAction', () => {
closeDb();