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:
160
src/cli/commands/journal.ts
Normal file
160
src/cli/commands/journal.ts
Normal 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}`);
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user