Add structured sections and hierarchical navigation
Add --section flag to add/update commands for structured node content, render sections and inline children in show, and add memory children command.
This commit is contained in:
@@ -11,6 +11,7 @@ export const addCommand = new Command('add')
|
||||
.option('-c, --content <content>', 'Node content/description', '')
|
||||
.option('--tags <tags>', 'Comma-separated tags')
|
||||
.option('--status <status>', 'Status (e.g. todo, doing, done, active, deprecated)')
|
||||
.option('--section <section...>', 'Structured section as "Label: body" (repeatable)')
|
||||
.description('Add a node to the knowledge graph')
|
||||
.action(async (kind: string, opts) => {
|
||||
if (!VALID_KINDS.includes(kind as NodeKind)) {
|
||||
@@ -19,12 +20,21 @@ export const addCommand = new Command('add')
|
||||
}
|
||||
|
||||
const tags = opts.tags ? opts.tags.split(',').map((t: string) => t.trim()) : [];
|
||||
const metadata: Record<string, any> = {};
|
||||
if (opts.section) {
|
||||
metadata.sections = (opts.section as string[]).map((s: string) => {
|
||||
const idx = s.indexOf(':');
|
||||
if (idx === -1) return { label: s.trim(), body: '' };
|
||||
return { label: s.slice(0, idx).trim(), body: s.slice(idx + 1).trim() };
|
||||
});
|
||||
}
|
||||
const node = await addNode({
|
||||
kind: kind as NodeKind,
|
||||
title: opts.title,
|
||||
content: opts.content,
|
||||
status: opts.status,
|
||||
tags,
|
||||
metadata,
|
||||
});
|
||||
|
||||
console.log(chalk.green('✓ Added node'));
|
||||
|
||||
45
src/cli/commands/children.ts
Normal file
45
src/cli/commands/children.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { findNodeByPrefix } from '../../core/store';
|
||||
import { getConnections } from '../../core/graph';
|
||||
import { NodeKind } from '../../types';
|
||||
|
||||
export const childrenCommand = new Command('children')
|
||||
.argument('<id>', 'Parent node ID (or prefix)')
|
||||
.option('--kind <kind>', 'Filter children by kind')
|
||||
.option('--format <fmt>', 'Output format: text or json', 'text')
|
||||
.description('List child nodes (outgoing "contains" edges)')
|
||||
.action(async (idRaw: string, opts) => {
|
||||
const node = findNodeByPrefix(idRaw);
|
||||
if (!node) {
|
||||
console.error(chalk.red(`Node not found: ${idRaw}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const conns = getConnections(node.id);
|
||||
let children = conns.outgoing.filter(c => c.type === 'contains');
|
||||
|
||||
if (opts.kind) {
|
||||
children = children.filter(c => c.node.kind === opts.kind);
|
||||
}
|
||||
|
||||
if (opts.format === 'json') {
|
||||
console.log(JSON.stringify(children.map(c => ({
|
||||
id: c.node.id,
|
||||
kind: c.node.kind,
|
||||
title: c.node.title,
|
||||
status: c.node.status,
|
||||
})), null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (children.length === 0) {
|
||||
console.log(chalk.dim('No children.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold(`Children of [${node.kind}] ${node.title}:`));
|
||||
for (const c of children) {
|
||||
console.log(` ${c.node.id.slice(0, 8)} [${c.node.kind}] ${c.node.title}`);
|
||||
}
|
||||
});
|
||||
@@ -30,6 +30,23 @@ export const showCommand = new Command('show')
|
||||
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) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const updateCommand = new Command('update')
|
||||
.option('--status <status>', 'New status')
|
||||
.option('--tags <tags>', 'Replace tags (comma-separated)')
|
||||
.option('--stale', 'Mark as stale')
|
||||
.option('--section <section...>', 'Structured section as "Label: body" (repeatable)')
|
||||
.description('Update a node')
|
||||
.action(async (idRaw: string, opts) => {
|
||||
const node = findNodeByPrefix(idRaw);
|
||||
@@ -23,9 +24,23 @@ export const updateCommand = new Command('update')
|
||||
if (opts.status !== undefined) input.status = opts.status;
|
||||
if (opts.tags !== undefined) input.tags = opts.tags.split(',').map((t: string) => t.trim());
|
||||
if (opts.stale) input.isStale = true;
|
||||
if (opts.section) {
|
||||
const newSections = (opts.section as string[]).map((s: string) => {
|
||||
const idx = s.indexOf(':');
|
||||
if (idx === -1) return { label: s.trim(), body: '' };
|
||||
return { label: s.slice(0, idx).trim(), body: s.slice(idx + 1).trim() };
|
||||
});
|
||||
const existing: { label: string; body: string }[] = node.metadata?.sections ?? [];
|
||||
for (const ns of newSections) {
|
||||
const idx = existing.findIndex((e) => e.label === ns.label);
|
||||
if (idx >= 0) existing[idx] = ns;
|
||||
else existing.push(ns);
|
||||
}
|
||||
input.metadata = { ...node.metadata, sections: existing };
|
||||
}
|
||||
|
||||
if (Object.keys(input).length === 0) {
|
||||
console.log(chalk.yellow('Nothing to update. Use --title, --content, --status, --tags, or --stale.'));
|
||||
console.log(chalk.yellow('Nothing to update. Use --title, --content, --status, --tags, --stale, or --section.'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { removeCommand } from './commands/remove';
|
||||
import { graphCommand } from './commands/graph';
|
||||
import { serveCommand } from './commands/serve';
|
||||
import { decayCommand } from './commands/decay';
|
||||
import { childrenCommand } from './commands/children';
|
||||
import { closeDb } from '../core/db';
|
||||
|
||||
const program = new Command();
|
||||
@@ -29,6 +30,7 @@ program.addCommand(removeCommand);
|
||||
program.addCommand(graphCommand);
|
||||
program.addCommand(serveCommand);
|
||||
program.addCommand(decayCommand);
|
||||
program.addCommand(childrenCommand);
|
||||
|
||||
program.hook('postAction', () => {
|
||||
closeDb();
|
||||
|
||||
Reference in New Issue
Block a user