diff --git a/USAGE.md b/USAGE.md index 02e746e..a0afa14 100644 --- a/USAGE.md +++ b/USAGE.md @@ -23,9 +23,11 @@ Add a node to the knowledge graph. | `-c, --content ` | no | Node content/description | | `--tags ` | no | Comma-separated tags | | `--status ` | no | Status (e.g. `todo`, `doing`, `done`, `active`, `deprecated`) | +| `--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: — 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 ` | no | New status | | `--tags ` | no | Replace tags (comma-separated) | | `--stale` | no | Mark as stale | +| `--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 ` + +List child nodes connected via outgoing `contains` edges from the given node. + +| Argument / Option | Required | Description | +|---|---|---| +| `` | yes | Parent node ID or prefix | +| `--kind ` | no | Filter children by kind | +| `--format ` | 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 ── + — 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 ` to list only children. + ## Search Scoring Search combines three signals: diff --git a/src/cli/commands/add.ts b/src/cli/commands/add.ts index 70dd143..fc415bd 100644 --- a/src/cli/commands/add.ts +++ b/src/cli/commands/add.ts @@ -11,6 +11,7 @@ export const addCommand = new Command('add') .option('-c, --content ', 'Node content/description', '') .option('--tags ', 'Comma-separated tags') .option('--status ', 'Status (e.g. todo, doing, done, active, deprecated)') + .option('--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 = {}; + 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')); diff --git a/src/cli/commands/children.ts b/src/cli/commands/children.ts new file mode 100644 index 0000000..0fa6629 --- /dev/null +++ b/src/cli/commands/children.ts @@ -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('', 'Parent node ID (or prefix)') + .option('--kind ', 'Filter children by kind') + .option('--format ', '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}`); + } + }); diff --git a/src/cli/commands/show.ts b/src/cli/commands/show.ts index 69688bb..6220c8c 100644 --- a/src/cli/commands/show.ts +++ b/src/cli/commands/show.ts @@ -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) { diff --git a/src/cli/commands/update.ts b/src/cli/commands/update.ts index 3dd3f74..bef2c6a 100644 --- a/src/cli/commands/update.ts +++ b/src/cli/commands/update.ts @@ -9,6 +9,7 @@ export const updateCommand = new Command('update') .option('--status ', 'New status') .option('--tags ', 'Replace tags (comma-separated)') .option('--stale', 'Mark as stale') + .option('--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; } diff --git a/src/cli/index.ts b/src/cli/index.ts index dadfa67..d15b2f9 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -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();