Enable time-travel queries and history viewing by creating immutable version records on every node update. Includes database schema changes, store functions, MCP tools, and CLI commands for viewing history, comparing versions, and restoring to previous states.
106 lines
4.2 KiB
TypeScript
106 lines
4.2 KiB
TypeScript
import { Command } from 'commander';
|
|
import chalk from 'chalk';
|
|
import { findNodeByPrefix, getNodeAtTime, getCurrentVersion } from '../../core/store';
|
|
import { getConnections } from '../../core/graph';
|
|
|
|
export const showCommand = new Command('show')
|
|
.argument('<id>', 'Node ID (or prefix)')
|
|
.option('--format <fmt>', 'Output format: text or json', 'text')
|
|
.option('--at <timestamp>', 'Show node at a specific point in time (ISO date or timestamp)')
|
|
.description('Show a node and its connections')
|
|
.action(async (idRaw: string, opts) => {
|
|
const node = findNodeByPrefix(idRaw);
|
|
if (!node) {
|
|
console.error(chalk.red(`Node not found: ${idRaw}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
// If --at is specified, show historical state
|
|
if (opts.at) {
|
|
const ts = isNaN(Number(opts.at)) ? Date.parse(opts.at) : Number(opts.at);
|
|
if (isNaN(ts)) {
|
|
console.error(chalk.red('Invalid timestamp format. Use ISO date (e.g., 2024-01-01) or Unix timestamp.'));
|
|
process.exit(1);
|
|
}
|
|
|
|
const historical = getNodeAtTime(node.id, ts);
|
|
if (!historical) {
|
|
console.error(chalk.red('No version found for the specified time.'));
|
|
process.exit(1);
|
|
}
|
|
|
|
if (opts.format === 'json') {
|
|
console.log(JSON.stringify(historical, null, 2));
|
|
return;
|
|
}
|
|
|
|
console.log(chalk.dim(`Viewing historical state at: ${new Date(ts).toLocaleString()}`));
|
|
console.log('');
|
|
console.log(chalk.bold.cyan(`[${historical.kind}] ${historical.title}`));
|
|
console.log(`ID: ${historical.id}`);
|
|
console.log(`Version: v${historical.version}`);
|
|
if (historical.status) console.log(`Status: ${historical.status}`);
|
|
if (historical.tags.length) console.log(`Tags: ${historical.tags.join(', ')}`);
|
|
console.log(`Valid: ${new Date(historical.validFrom).toLocaleString()} - ${historical.validUntil ? new Date(historical.validUntil).toLocaleString() : 'current'}`);
|
|
if (historical.content) console.log(`\n${historical.content}`);
|
|
|
|
// Render structured sections
|
|
if (historical.metadata?.sections && Array.isArray(historical.metadata.sections)) {
|
|
for (const sec of historical.metadata.sections) {
|
|
console.log(`\n${chalk.bold(`-- ${sec.label} --`)}`);
|
|
if (sec.body) console.log(sec.body);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
const conns = getConnections(node.id);
|
|
const version = getCurrentVersion(node.id);
|
|
|
|
if (opts.format === 'json') {
|
|
console.log(JSON.stringify({ ...node, embedding: undefined, version, connections: conns }, null, 2));
|
|
return;
|
|
}
|
|
|
|
console.log(chalk.bold.cyan(`[${node.kind}] ${node.title}`));
|
|
console.log(`ID: ${node.id}`);
|
|
console.log(`Version: v${version}`);
|
|
if (node.status) console.log(`Status: ${node.status}`);
|
|
if (node.tags.length) console.log(`Tags: ${node.tags.join(', ')}`);
|
|
console.log(`Created: ${new Date(node.createdAt).toLocaleString()}`);
|
|
console.log(`Updated: ${new Date(node.updatedAt).toLocaleString()}`);
|
|
if (node.isStale) console.log(chalk.red('STALE'));
|
|
if (node.content) console.log(`\n${node.content}`);
|
|
|
|
// Render structured sections
|
|
if (node.metadata?.sections && Array.isArray(node.metadata.sections)) {
|
|
for (const sec of node.metadata.sections) {
|
|
console.log(`\n${chalk.bold(`── ${sec.label} ──`)}`);
|
|
if (sec.body) console.log(sec.body);
|
|
}
|
|
}
|
|
|
|
// Inline children (outgoing 'contains' edges)
|
|
const children = conns.outgoing.filter(c => c.type === 'contains');
|
|
if (children.length) {
|
|
console.log(chalk.bold('\nChildren:'));
|
|
for (const c of children) {
|
|
console.log(` ${c.node.id.slice(0, 8)} [${c.node.kind}] ${c.node.title}`);
|
|
}
|
|
}
|
|
|
|
if (conns.outgoing.length) {
|
|
console.log(chalk.bold('\nOutgoing:'));
|
|
for (const c of conns.outgoing) {
|
|
console.log(` ${chalk.dim(`-[${c.type}]->`)} [${c.node.kind}] ${c.node.title} (${c.node.id.slice(0, 8)})`);
|
|
}
|
|
}
|
|
|
|
if (conns.incoming.length) {
|
|
console.log(chalk.bold('\nIncoming:'));
|
|
for (const c of conns.incoming) {
|
|
console.log(` [${c.node.kind}] ${c.node.title} (${c.node.id.slice(0, 8)}) ${chalk.dim(`-[${c.type}]->`)} this`);
|
|
}
|
|
}
|
|
});
|