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:
35
USAGE.md
35
USAGE.md
@@ -23,9 +23,11 @@ Add a node to the knowledge graph.
|
||||
| `-c, --content <content>` | no | Node content/description |
|
||||
| `--tags <tags>` | no | Comma-separated tags |
|
||||
| `--status <status>` | no | Status (e.g. `todo`, `doing`, `done`, `active`, `deprecated`) |
|
||||
| `--section <section>` | no | Structured section as `"Label: body"` (repeatable) |
|
||||
|
||||
```bash
|
||||
memory add memory -t "Auth flow design" -c "OAuth2 PKCE flow for SPA" --tags auth,security --status active
|
||||
memory add memory -t "add command" --section "Arguments: <kind> — memory, component, task, decision" --section "Options: -t title, -c content, --tags, --status"
|
||||
memory add task -t "Implement login page" --status todo
|
||||
memory add decision -t "Use PostgreSQL" -c "Chose Postgres over MySQL for JSON support"
|
||||
memory add component -t "UserService" -c "Handles user CRUD operations" --tags backend
|
||||
@@ -97,6 +99,7 @@ Update an existing node's fields.
|
||||
| `--status <status>` | no | New status |
|
||||
| `--tags <tags>` | no | Replace tags (comma-separated) |
|
||||
| `--stale` | no | Mark as stale |
|
||||
| `--section <section>` | no | Structured section as `"Label: body"` (repeatable, replaces by label) |
|
||||
|
||||
```bash
|
||||
memory update abc123 --status done
|
||||
@@ -146,6 +149,22 @@ memory graph
|
||||
memory graph abc123
|
||||
```
|
||||
|
||||
### `memory children <id>`
|
||||
|
||||
List child nodes connected via outgoing `contains` edges from the given node.
|
||||
|
||||
| Argument / Option | Required | Description |
|
||||
|---|---|---|
|
||||
| `<id>` | yes | Parent node ID or prefix |
|
||||
| `--kind <kind>` | no | Filter children by kind |
|
||||
| `--format <fmt>` | no | Output format: `text` or `json` (default: `text`) |
|
||||
|
||||
```bash
|
||||
memory children abc123
|
||||
memory children abc123 --kind task
|
||||
memory children abc123 --format json
|
||||
```
|
||||
|
||||
### `memory decay`
|
||||
|
||||
Run auto-decay to mark old untouched nodes as stale. Nodes whose `lastAccessedAt` exceeds the threshold are marked stale and hidden from default listings and search.
|
||||
@@ -197,6 +216,22 @@ memory serve --port 8080
|
||||
| `supersedes` | Source supersedes/replaces target |
|
||||
| `about` | Source is about target |
|
||||
|
||||
## Structured Sections
|
||||
|
||||
Nodes can have structured content via `metadata.sections`. Each section has a `label` and `body`. Use the `--section "Label: body"` flag on `add` or `update` to create sections.
|
||||
|
||||
When displayed with `memory show`, sections render as:
|
||||
|
||||
```
|
||||
── Arguments ──
|
||||
<kind> — memory, component, task, decision
|
||||
|
||||
── Options ──
|
||||
-t title, -c content, --tags, --status
|
||||
```
|
||||
|
||||
The `show` command also displays inline children (nodes linked via `contains` edges). Use `memory children <id>` to list only children.
|
||||
|
||||
## Search Scoring
|
||||
|
||||
Search combines three signals:
|
||||
|
||||
@@ -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