lore-memory 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/lore.js CHANGED
@@ -9,7 +9,98 @@ program
9
9
  .name('lore')
10
10
  .description('Persistent project memory for developers')
11
11
  .version('0.1.0')
12
- .action(() => program.outputHelp());
12
+ .action(async () => {
13
+ // Only launch the interactive menu if strictly NO arguments were provided
14
+ if (process.argv.length !== 2) return;
15
+
16
+ const inquirer = require('inquirer');
17
+ const chalk = require('chalk');
18
+ const { execSync } = require('child_process');
19
+
20
+ const LORE_LOGO = `
21
+ ██╗ ██████╗ ██████╗ ███████╗
22
+ ██║ ██╔═══██╗██╔══██╗██╔════╝
23
+ ██║ ██║ ██║██████╔╝█████╗
24
+ ██║ ██║ ██║██╔══██╗██╔══╝
25
+ ███████╗╚██████╔╝██║ ██║███████╗
26
+ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
27
+ `;
28
+
29
+ console.log(chalk.cyan(LORE_LOGO));
30
+ console.log(chalk.dim(' Project Memory for Developers\n'));
31
+
32
+ const { action } = await inquirer.prompt([
33
+ {
34
+ type: 'list',
35
+ name: 'action',
36
+ message: 'What would you like to do?',
37
+ choices: [
38
+ { name: '📝 Log new knowledge (lore log)', value: 'log' },
39
+ { name: '👀 Review pending drafts (lore drafts)', value: 'drafts' },
40
+ { name: '📊 View project health (lore score)', value: 'score' },
41
+ { name: '🔍 Search knowledge base (lore search)', value: 'search' },
42
+ { name: '🌐 Open Local Web Dashboard (lore ui)', value: 'ui' },
43
+ { name: '⚙️ Start background watcher (lore watch --daemon)', value: 'watch --daemon' },
44
+ new inquirer.Separator(),
45
+ { name: '❓ Show Help', value: 'help' },
46
+ { name: '❌ Exit', value: 'exit' }
47
+ ],
48
+ },
49
+ ]);
50
+
51
+ if (action === 'exit') {
52
+ process.exit(0);
53
+ } else if (action === 'help') {
54
+ program.outputHelp();
55
+ } else {
56
+ console.log();
57
+ try {
58
+ execSync(`node ${__filename} ${action}`, { stdio: 'inherit' });
59
+ } catch (e) { }
60
+ }
61
+ });
62
+
63
+ // Fuzzy matching for unknown commands
64
+ program.on('command:*', function (operands) {
65
+ const chalk = require('chalk');
66
+ console.error(chalk.red(`error: unknown command '${operands[0]}'`));
67
+ const availableCommands = program.commands.map(cmd => cmd.name());
68
+
69
+ // Simple Levenshtein distance check for did-you-mean
70
+ let closest = null;
71
+ let minDistance = 3; // only suggest if distance < 3
72
+
73
+ for (const cmd of availableCommands) {
74
+ let distance = 0;
75
+ const a = operands[0];
76
+ const b = cmd;
77
+ const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));
78
+ for (let i = 0; i <= a.length; i += 1) { matrix[0][i] = i; }
79
+ for (let j = 0; j <= b.length; j += 1) { matrix[j][0] = j; }
80
+ for (let j = 1; j <= b.length; j += 1) {
81
+ for (let i = 1; i <= a.length; i += 1) {
82
+ const indicator = a[i - 1] === b[j - 1] ? 0 : 1;
83
+ matrix[j][i] = Math.min(
84
+ matrix[j][i - 1] + 1,
85
+ matrix[j - 1][i] + 1,
86
+ matrix[j - 1][i - 1] + indicator
87
+ );
88
+ }
89
+ }
90
+ distance = matrix[b.length][a.length];
91
+
92
+ if (distance < minDistance) {
93
+ minDistance = distance;
94
+ closest = cmd;
95
+ }
96
+ }
97
+
98
+ if (closest) {
99
+ console.log();
100
+ console.log(chalk.yellow(`Did you mean ${chalk.bold('lore ' + closest)}?`));
101
+ }
102
+ process.exitCode = 1;
103
+ });
13
104
 
14
105
  program
15
106
  .command('init')
@@ -105,4 +196,10 @@ program
105
196
  .option('--build', 'Rebuild the full graph from source')
106
197
  .action(require('../src/commands/graph'));
107
198
 
199
+ program
200
+ .command('ui')
201
+ .description('Start the local Lore web dashboard')
202
+ .option('-p, --port <port>', 'Port to run the UI server on', '3333')
203
+ .action(require('../src/commands/ui'));
204
+
108
205
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lore-memory",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Persistent project memory for developers. Captures decisions, invariants, gotchas, and graveyard entries — automatically and manually — and injects them into AI coding sessions.",
5
5
  "main": "bin/lore.js",
6
6
  "bin": {
@@ -43,11 +43,13 @@
43
43
  "chalk": "^4",
44
44
  "chokidar": "^3.6.0",
45
45
  "commander": "^11",
46
+ "express": "^5.2.1",
46
47
  "fs-extra": "^11",
47
48
  "glob": "^10.5.0",
48
49
  "inquirer": "^8",
49
50
  "js-yaml": "^4",
50
51
  "natural": "^6.12.0",
51
- "ollama": "^0.6.3"
52
+ "ollama": "^0.6.3",
53
+ "open": "^11.0.0"
52
54
  }
53
55
  }
@@ -38,8 +38,7 @@ async function drafts(options) {
38
38
 
39
39
  console.log(chalk.cyan(`\n📖 ${pending.length} pending draft${pending.length === 1 ? '' : 's'}\n`));
40
40
 
41
- for (let i = 0; i < pending.length; i++) {
42
- const draft = pending[i];
41
+ const choices = pending.map((draft, i) => {
43
42
  const conf = Math.round((draft.confidence || 0) * 100);
44
43
  const typeColor = {
45
44
  decision: chalk.blue,
@@ -48,97 +47,74 @@ async function drafts(options) {
48
47
  graveyard: chalk.dim,
49
48
  }[draft.suggestedType] || chalk.white;
50
49
 
51
- console.log(chalk.cyan(`[${i + 1}/${pending.length}] SUGGESTED: ${typeColor(draft.suggestedType.toUpperCase())} (confidence: ${conf}%)`));
52
- console.log(` ${chalk.bold('Title:')} ${draft.suggestedTitle}`);
53
- console.log(` ${chalk.bold('Evidence:')} ${draft.evidence}`);
54
- if (draft.files && draft.files.length > 0) {
55
- console.log(` ${chalk.bold('Files:')} ${draft.files.join(', ')}`);
50
+ // Create a nicely formatted display string
51
+ const title = draft.suggestedTitle.length > 50 ? draft.suggestedTitle.slice(0, 47) + '...' : draft.suggestedTitle;
52
+ const display = `${typeColor(draft.suggestedType.toUpperCase().padEnd(9))} | ${title.padEnd(50)} | Conf: ${conf}%`;
53
+
54
+ return {
55
+ name: display,
56
+ value: draft,
57
+ checked: conf >= 80 // Pre-check high confidence ones
58
+ };
59
+ });
60
+
61
+ let selectedDrafts;
62
+ try {
63
+ const ans = await inquirer.prompt([{
64
+ type: 'checkbox',
65
+ name: 'selected',
66
+ message: 'Select drafts to ACCEPT (unselected drafts will be kept pending):',
67
+ choices: choices,
68
+ pageSize: 15
69
+ }]);
70
+ selectedDrafts = ans.selected;
71
+ } catch (e) {
72
+ console.log(chalk.yellow('\nAborted.'));
73
+ return;
74
+ }
75
+
76
+ if (selectedDrafts.length === 0) {
77
+ console.log(chalk.yellow('\nNo drafts selected. Keeping all drafts pending.'));
78
+ } else {
79
+ console.log(chalk.cyan(`\nAccepting ${selectedDrafts.length} drafts...`));
80
+ for (const draft of selectedDrafts) {
81
+ const entry = acceptDraft(draft.draftId);
82
+ console.log(chalk.green(` ✓ ${draft.suggestedTitle} -> Saved as ${entry.id}`));
56
83
  }
57
- console.log();
84
+ }
58
85
 
59
- let done = false;
60
- while (!done) {
61
- let action;
62
- try {
63
- const ans = await inquirer.prompt([{
64
- type: 'list',
65
- name: 'action',
66
- message: 'Action:',
67
- choices: [
68
- { name: '[a] Accept', value: 'accept' },
69
- { name: '[e] Edit then save', value: 'edit' },
70
- { name: '[s] Skip', value: 'skip' },
71
- { name: '[d] Delete', value: 'delete' },
72
- { name: '[q] Quit', value: 'quit' },
73
- ],
74
- }]);
75
- action = ans.action;
76
- } catch (e) {
77
- console.log(chalk.yellow('\nAborted.'));
78
- return;
79
- }
86
+ // Ask about deletions for remaining
87
+ const remainingDrafts = pending.filter(p => !selectedDrafts.find(s => s.draftId === p.draftId));
88
+
89
+ if (remainingDrafts.length > 0) {
90
+ console.log();
91
+ let deleteSelection;
92
+ try {
93
+ const deleteAns = await inquirer.prompt([{
94
+ type: 'checkbox',
95
+ name: 'deletes',
96
+ message: 'Select drafts to permanently DELETE:',
97
+ choices: remainingDrafts.map(draft => ({
98
+ name: `${draft.suggestedType.toUpperCase().padEnd(9)} | ${draft.suggestedTitle}`,
99
+ value: draft
100
+ })),
101
+ pageSize: 10
102
+ }]);
103
+ deleteSelection = deleteAns.deletes;
104
+ } catch (e) {
105
+ return;
106
+ }
80
107
 
81
- if (action === 'accept') {
82
- const entry = acceptDraft(draft.draftId);
83
- console.log(chalk.green(` ✓ Saved as ${entry.id}`));
84
- done = true;
85
- } else if (action === 'edit') {
86
- let edited;
87
- try {
88
- edited = await inquirer.prompt([
89
- {
90
- type: 'list',
91
- name: 'type',
92
- message: 'Type:',
93
- choices: ['decision', 'invariant', 'gotcha', 'graveyard'],
94
- default: draft.suggestedType,
95
- },
96
- {
97
- type: 'input',
98
- name: 'title',
99
- message: 'Title:',
100
- default: draft.suggestedTitle,
101
- },
102
- {
103
- type: 'input',
104
- name: 'context',
105
- message: 'Context:',
106
- default: draft.evidence,
107
- },
108
- ]);
109
- } catch (e) {
110
- console.log(chalk.yellow('\nAborted.'));
111
- return;
112
- }
113
-
114
- // Update draft on disk, then accept
115
- const draftPath = path.join(LORE_DIR, 'drafts', `${draft.draftId}.json`);
116
- fs.writeJsonSync(draftPath, {
117
- ...draft,
118
- suggestedType: edited.type,
119
- suggestedTitle: edited.title,
120
- evidence: edited.context,
121
- }, { spaces: 2 });
122
-
123
- const entry = acceptDraft(draft.draftId);
124
- console.log(chalk.green(` ✓ Saved as ${entry.id}`));
125
- done = true;
126
- } else if (action === 'skip') {
127
- console.log(chalk.dim(' Skipped'));
128
- done = true;
129
- } else if (action === 'delete') {
108
+ if (deleteSelection.length > 0) {
109
+ console.log(chalk.yellow(`\nDeleting ${deleteSelection.length} drafts...`));
110
+ for (const draft of deleteSelection) {
130
111
  deleteDraft(draft.draftId);
131
- console.log(chalk.dim(' Deleted'));
132
- done = true;
133
- } else if (action === 'quit') {
134
- console.log(chalk.cyan('\n Remaining drafts saved. Run: lore drafts'));
135
- return;
112
+ console.log(chalk.dim(` Deleted: ${draft.suggestedTitle}`));
136
113
  }
137
114
  }
138
- console.log();
139
115
  }
140
116
 
141
- console.log(chalk.green('✓ All drafts reviewed'));
117
+ console.log(chalk.green('\nDraft review complete'));
142
118
  }
143
119
 
144
120
  module.exports = drafts;
@@ -9,10 +9,13 @@ const { LORE_DIR, emptyIndex } = require('../lib/index');
9
9
  const HOOK_CONTENT = `#!/bin/bash
10
10
  LINECOUNT=$(git diff HEAD~1 --shortstat 2>/dev/null | grep -o '[0-9]* insertion' | grep -o '[0-9]*' || echo 0)
11
11
  if [ "\${LINECOUNT:-0}" -gt 50 ]; then
12
- echo "📖 Lore: Significant change detected. Log it? (y/n)"
13
- read -r answer </dev/tty
14
- if [ "$answer" = "y" ]; then
15
- lore log </dev/tty
12
+ # Only prompt if running in an interactive terminal
13
+ if [ -t 1 ] || [ -t 0 ]; then
14
+ echo "📖 Lore: Significant change detected. Log it? (y/n)"
15
+ read -r answer </dev/tty
16
+ if [ "$answer" = "y" ]; then
17
+ lore log </dev/tty
18
+ fi
16
19
  fi
17
20
  fi
18
21
  `;
@@ -108,7 +108,7 @@ async function log(options) {
108
108
  id,
109
109
  type,
110
110
  title,
111
- date: new Date().toISOString().split('T')[0],
111
+ date: new Date().toISOString(),
112
112
  files,
113
113
  context,
114
114
  alternatives: alternatives.filter(Boolean),
@@ -6,7 +6,7 @@ const fs = require('fs-extra');
6
6
  const { mineFile, mineDirectory } = require('../watcher/comments');
7
7
  const { requireInit } = require('../lib/guard');
8
8
 
9
- function mine(targetPath) {
9
+ async function mine(targetPath) {
10
10
  requireInit();
11
11
  const projectRoot = process.cwd();
12
12
  const target = targetPath || '.';
@@ -18,10 +18,11 @@ function mine(targetPath) {
18
18
 
19
19
  if (stat.isDirectory()) {
20
20
  console.log(chalk.cyan(`📖 Mining comments in ${target} ...`));
21
- count = mineDirectory(abs, projectRoot);
21
+ count = await mineDirectory(abs, projectRoot);
22
22
  } else {
23
23
  console.log(chalk.cyan(`📖 Mining comments in ${target} ...`));
24
- count = mineFile(abs, projectRoot).length;
24
+ const drafts = await mineFile(abs, projectRoot);
25
+ count = drafts.length;
25
26
  }
26
27
 
27
28
  if (count === 0) {
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const { requireInit } = require('../lib/guard');
7
+ const { readIndex, LORE_DIR } = require('../lib/index');
8
+ const { readEntry } = require('../lib/entries');
9
+ const { computeScore } = require('../lib/scorer');
10
+ const { listDrafts, acceptDraft, deleteDraft } = require('../lib/drafts');
11
+
12
+ // Only load 'open' dynamically to avoid overhead on other CLI commands if not needed
13
+ async function openBrowser(url) {
14
+ const open = (await import('open')).default;
15
+ await open(url);
16
+ }
17
+
18
+ function ui(options) {
19
+ requireInit();
20
+
21
+ const app = express();
22
+ const PORT = options.port || 3333;
23
+
24
+ app.use(express.json());
25
+
26
+ // CORS for local dev
27
+ app.use((req, res, next) => {
28
+ res.header('Access-Control-Allow-Origin', '*');
29
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
30
+ res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
31
+ next();
32
+ });
33
+
34
+ // API Endpoints
35
+
36
+ app.get('/api/stats', (req, res) => {
37
+ try {
38
+ const scoreData = computeScore();
39
+ const drafts = listDrafts();
40
+
41
+ const index = readIndex();
42
+ const counts = { decision: 0, invariant: 0, graveyard: 0, gotcha: 0 };
43
+ for (const entryPath of Object.values(index.entries)) {
44
+ const entry = readEntry(entryPath);
45
+ if (entry && counts[entry.type] !== undefined) counts[entry.type]++;
46
+ }
47
+
48
+ res.json({
49
+ score: scoreData,
50
+ counts,
51
+ draftCount: drafts.length,
52
+ totalEntries: Object.keys(index.entries).length
53
+ });
54
+ } catch (e) {
55
+ res.status(500).json({ error: e.message });
56
+ }
57
+ });
58
+
59
+ app.get('/api/entries', (req, res) => {
60
+ try {
61
+ const index = readIndex();
62
+ const entries = [];
63
+ for (const entryPath of Object.values(index.entries)) {
64
+ const entry = readEntry(entryPath);
65
+ if (entry) entries.push(entry);
66
+ }
67
+ // Sort newest first
68
+ entries.sort((a, b) => new Date(b.date) - new Date(a.date));
69
+ res.json(entries);
70
+ } catch (e) {
71
+ res.status(500).json({ error: e.message });
72
+ }
73
+ });
74
+
75
+ app.get('/api/drafts', (req, res) => {
76
+ try {
77
+ const drafts = listDrafts();
78
+ res.json(drafts);
79
+ } catch (e) {
80
+ res.status(500).json({ error: e.message });
81
+ }
82
+ });
83
+
84
+ app.post('/api/drafts/:id/accept', (req, res) => {
85
+ try {
86
+ const entry = acceptDraft(req.params.id);
87
+ res.json({ success: true, entry });
88
+ } catch (e) {
89
+ res.status(500).json({ error: e.message });
90
+ }
91
+ });
92
+
93
+ app.delete('/api/drafts/:id', (req, res) => {
94
+ try {
95
+ deleteDraft(req.params.id);
96
+ res.json({ success: true });
97
+ } catch (e) {
98
+ res.status(500).json({ error: e.message });
99
+ }
100
+ });
101
+
102
+ app.get('/api/graph', (req, res) => {
103
+ try {
104
+ const { loadGraph, saveGraph } = require('../lib/graph');
105
+ let g = loadGraph();
106
+
107
+ if (Object.keys(g.imports).length === 0) {
108
+ const { buildFullGraph } = require('../watcher/graph');
109
+ g = buildFullGraph(process.cwd());
110
+ saveGraph(g);
111
+ }
112
+
113
+ const nodesSet = new Set();
114
+ const edges = [];
115
+ for (const [file, deps] of Object.entries(g.imports)) {
116
+ nodesSet.add(file);
117
+ for (const dep of deps) {
118
+ nodesSet.add(dep);
119
+ edges.push({ from: file, to: dep });
120
+ }
121
+ }
122
+
123
+ const nodes = Array.from(nodesSet).map(id => ({ id, label: id }));
124
+ res.json({ nodes, edges });
125
+ } catch (e) {
126
+ res.status(500).json({ error: e.message });
127
+ }
128
+ });
129
+
130
+ // Handle unmapped API routes with 404 JSON (instead of serving index.html)
131
+ app.use('/api', (req, res) => {
132
+ res.status(404).json({ error: 'API route not found' });
133
+ });
134
+
135
+ // Serve the frontend application
136
+ const uiPath = path.join(__dirname, '..', 'ui', 'public');
137
+ app.use(express.static(uiPath));
138
+
139
+ // Catch-all to serve index.html for SPA routing
140
+ app.use((req, res) => {
141
+ res.sendFile(path.join(uiPath, 'index.html'));
142
+ });
143
+
144
+ const server = app.listen(PORT, () => {
145
+ const url = `http://localhost:${PORT}`;
146
+ console.log(chalk.green(`\n🚀 Lore UI Dashboard running at ${chalk.bold(url)}\n`));
147
+ console.log(chalk.cyan(` Press Ctrl+C to stop the server.`));
148
+
149
+ // Use native exec to open browser to avoid ESM import issues with 'open'
150
+ const { exec } = require('child_process');
151
+ const startPath = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
152
+ exec(`${startPath} ${url}`, (err) => {
153
+ if (err) {
154
+ console.log(chalk.dim(` (Could not open browser automatically. Please visit ${url} manually)`));
155
+ }
156
+ });
157
+ });
158
+
159
+ server.on('error', (e) => {
160
+ if (e.code === 'EADDRINUSE') {
161
+ console.error(chalk.red(`\nPort ${PORT} is already in use by another process.`));
162
+ console.error(chalk.yellow(`Use 'lore ui --port <number>' to specify a different port.\n`));
163
+ process.exit(1);
164
+ } else {
165
+ console.error(chalk.red(`\nFailed to start server: ${e.message}\n`));
166
+ process.exit(1);
167
+ }
168
+ });
169
+ }
170
+
171
+ module.exports = ui;
package/src/lib/format.js CHANGED
@@ -2,22 +2,73 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
 
5
+ function drawBox(content, colorFn, title) {
6
+ const lines = content.split('\n');
7
+ const maxLen = Math.max(
8
+ ...lines.map(l => l.replace(/\x1B\[[0-9;]*m/g, '').length),
9
+ title ? title.length + 4 : 0
10
+ );
11
+
12
+ const width = Math.min(80, Math.max(40, maxLen + 2)); // keep between 40 and 80 width
13
+
14
+ const top = title
15
+ ? `╭─ ${title} ${'─'.repeat(Math.max(0, width - title.length - 4))}╮`
16
+ : `╭${'─'.repeat(width)}╮`;
17
+
18
+ console.log(colorFn(top));
19
+
20
+ for (const line of lines) {
21
+ const cleanLen = line.replace(/\x1B\[[0-9;]*m/g, '').length;
22
+ const padding = ' '.repeat(Math.max(0, width - cleanLen - 2));
23
+ console.log(colorFn('│ ') + line + padding + colorFn(' │'));
24
+ }
25
+
26
+ console.log(colorFn(`╰${'─'.repeat(width)}╯\n`));
27
+ }
28
+
5
29
  function printEntry(entry) {
30
+ let content = '';
31
+
32
+ let colorFn = chalk.white;
33
+ let titleColor = chalk.bold.white;
34
+
35
+ if (entry.type === 'decision') { colorFn = chalk.cyan; titleColor = chalk.bold.cyan; }
36
+ else if (entry.type === 'invariant') { colorFn = chalk.red; titleColor = chalk.bold.red; }
37
+ else if (entry.type === 'gotcha') { colorFn = chalk.yellow; titleColor = chalk.bold.yellow; }
38
+ else if (entry.type === 'graveyard') { colorFn = chalk.gray; titleColor = chalk.bold.gray; }
39
+
6
40
  const typeLabel = `[${entry.type.toUpperCase()}]`;
7
- console.log(chalk.bold(`${typeLabel} ${entry.title}`) + chalk.gray(` (${entry.date})`));
8
- console.log(` → ${entry.context}`);
41
+ content += `${titleColor(typeLabel)} ${titleColor(entry.title)} ${chalk.gray('(' + entry.date + ')')}\n\n`;
42
+
43
+ // Wrap context text roughly
44
+ const contextWords = entry.context.split(' ');
45
+ let currentLine = '';
46
+ for (const word of contextWords) {
47
+ if (currentLine.length + word.length > 70) {
48
+ content += chalk.white(currentLine) + '\n';
49
+ currentLine = word + ' ';
50
+ } else {
51
+ currentLine += word + ' ';
52
+ }
53
+ }
54
+ if (currentLine) content += chalk.white(currentLine) + '\n';
9
55
 
10
56
  if (entry.alternatives && entry.alternatives.length > 0) {
57
+ content += '\n' + chalk.yellow('Alternatives Considered:\n');
11
58
  for (const alt of entry.alternatives) {
12
- console.log(chalk.yellow(` → Rejected: ${alt}`));
59
+ content += chalk.yellow(` → ${alt}\n`);
13
60
  }
14
61
  }
15
62
 
16
63
  if (entry.tradeoffs) {
17
- console.log(chalk.gray(`Tradeoffs: ${entry.tradeoffs}`));
64
+ content += '\n' + chalk.magenta(`Tradeoffs: ${entry.tradeoffs}\n`);
65
+ }
66
+
67
+ if (entry.files && entry.files.length > 0) {
68
+ content += '\n' + chalk.dim(`Files: ${entry.files.join(', ')}\n`);
18
69
  }
19
70
 
20
- console.log();
71
+ drawBox(content.trim(), colorFn, entry.id);
21
72
  }
22
73
 
23
74
  module.exports = { printEntry };
@@ -45,6 +45,11 @@ async function handler(args) {
45
45
  }
46
46
  }
47
47
 
48
+ // Sort entries newest-first
49
+ for (const type of Object.keys(byType)) {
50
+ byType[type].sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0));
51
+ }
52
+
48
53
  const lines = [];
49
54
  const projectName = config.project || 'this project';
50
55
 
@@ -49,10 +49,20 @@ async function handler(args) {
49
49
  ].join(' ').toLowerCase();
50
50
 
51
51
  if (searchable.includes(q)) {
52
- matches.push(entry);
52
+ // Calculate a basic relevance score for text search:
53
+ // 1. Term frequency of query
54
+ const tf = (searchable.match(new RegExp(q, 'g')) || []).length;
55
+ // 2. Bonus for newer entries
56
+ const ageDays = (Date.now() - new Date(entry.date || 0)) / (1000 * 60 * 60 * 24);
57
+ const recencyBonus = Math.max(0, 5 - (ageDays / 30)); // up to +5 points for newest
58
+
59
+ matches.push(Object.assign({}, entry, { _score: tf + recencyBonus }));
53
60
  }
54
61
  }
55
62
 
63
+ // Sort matches by relevance score descending
64
+ matches.sort((a, b) => (b._score || 0) - (a._score || 0));
65
+
56
66
  // Try semantic search if text search found nothing and embeddings exist
57
67
  if (matches.length === 0) {
58
68
  try {