1- import { NodeRecord , deleteNode , updateNode , listNodes } from '../../lib/db' ;
1+ import { NodeRecord , deleteNode , updateNode , listNodes , listEdges } from '../../lib/db' ;
22import { EdgeRecord , EdgeStatus , insertOrUpdateEdge } from '../../lib/db' ;
33import { extractTags , tokenize } from '../../lib/text' ;
44import { computeEmbeddingForNode } from '../../lib/embeddings' ;
@@ -18,7 +18,7 @@ import { COMMAND_TLDR, emitTldrAndExit } from '../tldr';
1818import { synthesizeNodesCore , SynthesisModel , ReasoningEffort , TextVerbosity } from '../../core/synthesize' ;
1919import { createNodeCore } from '../../core/nodes' ;
2020import { importDocumentCore } from '../../core/import' ;
21- import { reconstructDocument } from '../../lib/reconstruction' ;
21+ import { reconstructDocument , filterOutChunks } from '../../lib/reconstruction' ;
2222
2323import type { HandlerContext } from '@clerc/core' ;
2424
@@ -79,6 +79,15 @@ type NodeImportFlags = {
7979 tldr ?: string ;
8080} ;
8181
82+ type NodeRecentFlags = {
83+ limit ?: number ;
84+ json ?: boolean ;
85+ created ?: boolean ;
86+ updated ?: boolean ;
87+ since ?: string ;
88+ tldr ?: string ;
89+ } ;
90+
8291export function registerNodeCommands ( cli : ClercInstance , clerc : ClercModule ) {
8392 const readCommand = clerc . defineCommand (
8493 {
@@ -365,6 +374,53 @@ export function registerNodeCommands(cli: ClercInstance, clerc: ClercModule) {
365374 ) ;
366375 cli . command ( importCommand ) ;
367376
377+ const recentCommand = clerc . defineCommand (
378+ {
379+ name : 'node recent' ,
380+ description : 'Show recent node activity (created/updated)' ,
381+ flags : {
382+ limit : {
383+ type : Number ,
384+ description : 'Maximum number of activities to show' ,
385+ default : 20 ,
386+ } ,
387+ json : {
388+ type : Boolean ,
389+ description : 'Emit JSON output' ,
390+ } ,
391+ created : {
392+ type : Boolean ,
393+ description : 'Show only created activities' ,
394+ } ,
395+ updated : {
396+ type : Boolean ,
397+ description : 'Show only updated activities' ,
398+ } ,
399+ since : {
400+ type : String ,
401+ description : 'Show activities since duration (e.g., 24h, 7d, 4w, 1m, 1y)' ,
402+ } ,
403+ tldr : {
404+ type : String ,
405+ description : 'Output command metadata for agent consumption (--tldr or --tldr=json)' ,
406+ } ,
407+ } ,
408+ } ,
409+ async ( { flags } : { flags : NodeRecentFlags } ) => {
410+ try {
411+ // Handle TLDR request first
412+ if ( flags . tldr !== undefined ) {
413+ const jsonMode = flags . tldr === 'json' ;
414+ emitTldrAndExit ( COMMAND_TLDR [ 'node.recent' ] , jsonMode ) ;
415+ }
416+ await runNodeRecent ( flags ) ;
417+ } catch ( error ) {
418+ handleError ( error ) ;
419+ }
420+ } ,
421+ ) ;
422+ cli . command ( recentCommand ) ;
423+
368424 const baseCommand = clerc . defineCommand (
369425 {
370426 name : 'node' ,
@@ -376,6 +432,7 @@ export function registerNodeCommands(cli: ClercInstance, clerc: ClercModule) {
376432 ' edit Edit an existing note and optionally rescore links' ,
377433 ' delete Delete a note and its edges' ,
378434 ' link Manually create an edge between two notes' ,
435+ ' recent Show recent node activity (created/updated)' ,
379436 ' import Import a large markdown document by chunking it' ,
380437 ' synthesize Use GPT-5 to synthesize a new article from 2+ notes' ,
381438 '' ,
@@ -903,6 +960,126 @@ async function runNodeImport(flags: NodeImportFlags) {
903960 console . log ( '' ) ;
904961}
905962
963+ async function runNodeRecent ( flags : NodeRecentFlags ) {
964+ const limit =
965+ typeof flags . limit === 'number' && ! Number . isNaN ( flags . limit ) && flags . limit > 0 ? flags . limit : 20 ;
966+
967+ // Get all nodes and filter out chunks
968+ const allNodes = await listNodes ( ) ;
969+ const nodes = filterOutChunks ( allNodes ) ;
970+
971+ // Get all accepted edges for counting
972+ const acceptedEdges = await listEdges ( 'accepted' ) ;
973+
974+ // Build edge count map
975+ const edgeCountMap = new Map < string , number > ( ) ;
976+ for ( const edge of acceptedEdges ) {
977+ edgeCountMap . set ( edge . sourceId , ( edgeCountMap . get ( edge . sourceId ) || 0 ) + 1 ) ;
978+ edgeCountMap . set ( edge . targetId , ( edgeCountMap . get ( edge . targetId ) || 0 ) + 1 ) ;
979+ }
980+
981+ // Build timeline: each node generates activity records
982+ type Activity = {
983+ type : 'created' | 'updated' ;
984+ timestamp : string ;
985+ node : NodeRecord ;
986+ } ;
987+
988+ const activities : Activity [ ] = [ ] ;
989+ for ( const node of nodes ) {
990+ // Add created activity unless user specified --updated only
991+ if ( ! flags . updated ) {
992+ activities . push ( { type : 'created' , timestamp : node . createdAt , node } ) ;
993+ }
994+
995+ // Add updated activity if it's different from created, unless user specified --created only
996+ if ( ! flags . created && node . createdAt !== node . updatedAt ) {
997+ activities . push ( { type : 'updated' , timestamp : node . updatedAt , node } ) ;
998+ }
999+ }
1000+
1001+ // Apply --since filter if provided
1002+ let filteredActivities = activities ;
1003+ if ( flags . since ) {
1004+ const sinceMs = parseDuration ( flags . since ) ;
1005+ filteredActivities = activities . filter ( ( activity ) => {
1006+ const activityMs = new Date ( activity . timestamp ) . getTime ( ) ;
1007+ return activityMs >= sinceMs ;
1008+ } ) ;
1009+ }
1010+
1011+ // Sort by timestamp descending (most recent first)
1012+ filteredActivities . sort ( ( a , b ) => new Date ( b . timestamp ) . getTime ( ) - new Date ( a . timestamp ) . getTime ( ) ) ;
1013+
1014+ // Take limit
1015+ const recentActivities = filteredActivities . slice ( 0 , limit ) ;
1016+
1017+ if ( recentActivities . length === 0 ) {
1018+ console . log ( 'No recent node activity found.' ) ;
1019+ return ;
1020+ }
1021+
1022+ // Emit JSON or table output
1023+ if ( flags . json ) {
1024+ console . log (
1025+ JSON . stringify (
1026+ recentActivities . map ( ( activity ) => ( {
1027+ type : activity . type ,
1028+ timestamp : activity . timestamp ,
1029+ node : {
1030+ id : activity . node . id ,
1031+ title : activity . node . title ,
1032+ tags : activity . node . tags ,
1033+ bodyPreview : activity . node . body . slice ( 0 , 100 ) ,
1034+ edges : edgeCountMap . get ( activity . node . id ) || 0 ,
1035+ } ,
1036+ } ) ) ,
1037+ null ,
1038+ 2
1039+ )
1040+ ) ;
1041+ return ;
1042+ }
1043+
1044+ // Table output
1045+ console . log ( `\nRecent node activity (${ recentActivities . length } ):\n` ) ;
1046+
1047+ for ( const activity of recentActivities ) {
1048+ const date = new Date ( activity . timestamp ) . toISOString ( ) . replace ( 'T' , ' ' ) . slice ( 0 , 19 ) ;
1049+ const typeLabel = activity . type . padEnd ( 7 ) ;
1050+ const shortId = formatId ( activity . node . id ) ;
1051+ const title = activity . node . title . length > 40 ? activity . node . title . slice ( 0 , 37 ) + '...' : activity . node . title ;
1052+ const tagsStr = activity . node . tags . length > 0 ? `[${ activity . node . tags . slice ( 0 , 3 ) . join ( ', ' ) } ]` : '' ;
1053+ const bodyPreview = activity . node . body . replace ( / \n / g, ' ' ) . slice ( 0 , 100 ) ;
1054+ const edgeCount = edgeCountMap . get ( activity . node . id ) || 0 ;
1055+ const edgesLabel = `${ edgeCount } edge${ edgeCount === 1 ? '' : 's' } ` ;
1056+
1057+ console . log ( `${ date } ${ typeLabel } ${ shortId } ${ title . padEnd ( 42 ) } ${ tagsStr } ` ) ;
1058+ console . log ( `${ ' ' . repeat ( 42 ) } ${ bodyPreview } ${ edgesLabel } ` ) ;
1059+ console . log ( '' ) ;
1060+ }
1061+ }
1062+
1063+ function parseDuration ( duration : string ) : number {
1064+ const match = duration . match ( / ^ ( \d + ) ( [ h d w m y ] ) $ / ) ;
1065+ if ( ! match ) {
1066+ throw new Error ( `Invalid duration format: "${ duration } ". Use format like: 24h, 7d, 4w, 1m, 1y` ) ;
1067+ }
1068+
1069+ const value = Number ( match [ 1 ] ) ;
1070+ const unit = match [ 2 ] ;
1071+
1072+ const msPerUnit : Record < string , number > = {
1073+ h : 3600000 , // 1 hour
1074+ d : 86400000 , // 1 day
1075+ w : 604800000 , // 1 week
1076+ m : 2592000000 , // 30 days
1077+ y : 31536000000 , // 365 days
1078+ } ;
1079+
1080+ return Date . now ( ) - ( value * msPerUnit [ unit ] ) ;
1081+ }
1082+
9061083function validateChunkStrategy ( strategyFlag : string | undefined ) : 'headers' | 'size' | 'hybrid' {
9071084 if ( ! strategyFlag ) return 'headers' ;
9081085 const normalized = strategyFlag . toLowerCase ( ) ;
0 commit comments