lore-memory 0.3.0 → 0.5.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
@@ -8,7 +8,7 @@ const program = new Command();
8
8
  program
9
9
  .name('lore')
10
10
  .description('Persistent project memory for developers')
11
- .version('0.1.0')
11
+ .version(require('../package.json').version)
12
12
  .action(async () => {
13
13
  // Only launch the interactive menu if strictly NO arguments were provided
14
14
  if (process.argv.length !== 2) return;
@@ -39,6 +39,7 @@ program
39
39
  { name: '👀 Review pending drafts (lore drafts)', value: 'drafts' },
40
40
  { name: '📊 View project health (lore score)', value: 'score' },
41
41
  { name: '🔍 Search knowledge base (lore search)', value: 'search' },
42
+ { name: '🪄 Generate AI Prompt (lore prompt)', value: 'prompt' },
42
43
  { name: '🌐 Open Local Web Dashboard (lore ui)', value: 'ui' },
43
44
  { name: '⚙️ Start background watcher (lore watch --daemon)', value: 'watch --daemon' },
44
45
  new inquirer.Separator(),
@@ -52,6 +53,18 @@ program
52
53
  process.exit(0);
53
54
  } else if (action === 'help') {
54
55
  program.outputHelp();
56
+ } else if (action === 'prompt') {
57
+ const { query } = await inquirer.prompt([{
58
+ type: 'input',
59
+ name: 'query',
60
+ message: 'What are you trying to build or refactor? (e.g. "Add a login page")'
61
+ }]);
62
+ console.log();
63
+ if (query.trim()) {
64
+ try {
65
+ execSync(`node ${__filename} prompt "${query.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
66
+ } catch (e) { }
67
+ }
55
68
  } else {
56
69
  console.log();
57
70
  try {
@@ -135,7 +148,7 @@ program
135
148
  .action(require('../src/commands/stale'));
136
149
 
137
150
  program
138
- .command('search <query>')
151
+ .command('search [query...]')
139
152
  .description('Search all entries for a keyword')
140
153
  .action(require('../src/commands/search'));
141
154
 
@@ -151,8 +164,9 @@ program
151
164
 
152
165
  program
153
166
  .command('serve')
154
- .description('Start the Lore MCP server (stdio) for use with Claude Code')
167
+ .description('Start the Lore MCP server (stdio) and UI dashboard for use with Claude Code')
155
168
  .option('-q, --quiet', 'Suppress startup messages (use when piped into MCP client)')
169
+ .option('-p, --port <port>', 'Port for the UI dashboard', '3333')
156
170
  .action(require('../src/commands/serve'));
157
171
 
158
172
  program
@@ -202,4 +216,11 @@ program
202
216
  .option('-p, --port <port>', 'Port to run the UI server on', '3333')
203
217
  .action(require('../src/commands/ui'));
204
218
 
219
+ program
220
+ .command('prompt [query...]')
221
+ .description('Generate a perfectly formatted LLM context prompt from project memory')
222
+ .option('-t, --threshold <number>', 'Relevance threshold (0.0 to 1.0)', '0.4')
223
+ .option('-l, --limit <number>', 'Max number of entries to include', '10')
224
+ .action(require('../src/commands/prompt'));
225
+
205
226
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lore-memory",
3
- "version": "0.3.0",
3
+ "version": "0.5.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": {
@@ -6,20 +6,6 @@ const yaml = require('js-yaml');
6
6
  const chalk = require('chalk');
7
7
  const { LORE_DIR, emptyIndex } = require('../lib/index');
8
8
 
9
- const HOOK_CONTENT = `#!/bin/bash
10
- LINECOUNT=$(git diff HEAD~1 --shortstat 2>/dev/null | grep -o '[0-9]* insertion' | grep -o '[0-9]*' || echo 0)
11
- if [ "\${LINECOUNT:-0}" -gt 50 ]; then
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
19
- fi
20
- fi
21
- `;
22
-
23
9
  async function init() {
24
10
  try {
25
11
  const dirs = ['decisions', 'invariants', 'graveyard', 'gotchas', 'modules', 'sessions', 'drafts'];
@@ -44,6 +30,7 @@ async function init() {
44
30
  },
45
31
  mcp: {
46
32
  tokenBudget: 4000,
33
+ confirmEntries: true,
47
34
  },
48
35
  watchMode: false,
49
36
  watchIgnore: ['node_modules', 'dist', '.git', 'coverage'],
@@ -64,16 +51,6 @@ async function init() {
64
51
  );
65
52
  }
66
53
 
67
- const hookDir = path.join('.git', 'hooks');
68
- if (fs.existsSync(hookDir)) {
69
- const hookPath = path.join(hookDir, 'post-commit');
70
- await fs.writeFile(hookPath, HOOK_CONTENT);
71
- await fs.chmod(hookPath, '755');
72
- console.log(chalk.green('✓ Git post-commit hook installed'));
73
- } else {
74
- console.log(chalk.yellow('⚠ Not a git repo — hook not installed'));
75
- }
76
-
77
54
  console.log(chalk.green(`✓ Lore initialized at ${LORE_DIR}/`));
78
55
  console.log(chalk.cyan(' Run: lore mine . to scan codebase for lore-worthy comments'));
79
56
  console.log(chalk.cyan(' Run: lore log to create your first entry manually'));
@@ -95,7 +72,7 @@ async function init() {
95
72
  console.log(chalk.dim('\n Scanning codebase for lore-worthy comments...'));
96
73
  try {
97
74
  const { mineDirectory } = require('../watcher/comments');
98
- const count = mineDirectory(process.cwd(), process.cwd());
75
+ const count = await mineDirectory(process.cwd(), process.cwd());
99
76
  if (count > 0) {
100
77
  console.log(chalk.cyan(` Found ${count} draft entr${count === 1 ? 'y' : 'ies'} — run: lore drafts`));
101
78
  }
@@ -4,7 +4,7 @@ const chalk = require('chalk');
4
4
  const inquirer = require('inquirer');
5
5
  const { readIndex, writeIndex, addEntryToIndex } = require('../lib/index');
6
6
  const { requireInit } = require('../lib/guard');
7
- const { writeEntry, generateId } = require('../lib/entries');
7
+ const { writeEntry, generateId, findDuplicate } = require('../lib/entries');
8
8
  const { getRecentFiles } = require('../lib/git');
9
9
 
10
10
  async function log(options) {
@@ -103,6 +103,32 @@ async function log(options) {
103
103
  process.exit(1);
104
104
  }
105
105
 
106
+ // Deduplication check
107
+ const duplicate = findDuplicate(index, type, title);
108
+ if (duplicate) {
109
+ const matchLabel = duplicate.match === 'exact' ? 'Exact duplicate' : 'Similar entry';
110
+ console.log(chalk.yellow(`\n⚠ ${matchLabel} found: ${chalk.bold(duplicate.entry.id)}`));
111
+ console.log(chalk.dim(` Title: "${duplicate.entry.title}"\n`));
112
+
113
+ // In inline mode (flags), just warn and exit
114
+ if (options.type && options.title && options.context) {
115
+ console.log(chalk.yellow('Skipping to avoid duplicate. Use a different title or edit the existing entry.'));
116
+ return;
117
+ }
118
+
119
+ // In interactive mode, ask user
120
+ const { proceed } = await inquirer.prompt([{
121
+ type: 'confirm',
122
+ name: 'proceed',
123
+ message: 'Create entry anyway?',
124
+ default: false,
125
+ }]);
126
+ if (!proceed) {
127
+ console.log(chalk.dim('Aborted.'));
128
+ return;
129
+ }
130
+ }
131
+
106
132
  const id = generateId(type, title);
107
133
  const entry = {
108
134
  id,
@@ -58,8 +58,9 @@ function onboard(options) {
58
58
  for (const e of byType.decision.slice(0, 5)) {
59
59
  console.log(chalk.white(` • ${e.title}`));
60
60
  if (e.context) {
61
- const summary = e.context.split('\n')[0].slice(0, 80);
62
- console.log(chalk.dim(` ${summary}${e.context.length > 80 ? '…' : ''}`));
61
+ const firstLine = e.context.split('\n')[0];
62
+ const summary = firstLine.slice(0, 80);
63
+ console.log(chalk.dim(` ${summary}${firstLine.length > 80 ? '…' : ''}`));
63
64
  }
64
65
  }
65
66
  if (byType.decision.length > 5) {
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const { requireInit } = require('../lib/guard');
5
+ const { findSimilar } = require('../lib/embeddings');
6
+ const { readIndex } = require('../lib/index');
7
+ const { readEntry } = require('../lib/entries');
8
+ const { formatPromptContext } = require('../lib/format');
9
+
10
+ async function promptCmd(queryArgs, options) {
11
+ requireInit();
12
+
13
+ const queryInfo = Array.isArray(queryArgs) ? queryArgs.join(' ') : (queryArgs || '');
14
+
15
+ if (!queryInfo || queryInfo.trim().length === 0) {
16
+ console.error(chalk.red('\nError: You must provide a query to generate a prompt.'));
17
+ console.log(chalk.yellow('Example: lore prompt "I want to refactor the database"'));
18
+ process.exit(1);
19
+ }
20
+
21
+ const query = queryInfo;
22
+
23
+ try {
24
+ const threshold = parseFloat(options.threshold) || 0.4;
25
+ const limit = parseInt(options.limit, 10) || 10;
26
+
27
+ const index = readIndex();
28
+ const allIds = Object.keys(index.entries);
29
+
30
+ let results = [];
31
+ try {
32
+ const similar = await findSimilar(query, allIds, limit);
33
+ for (const { id, score } of similar) {
34
+ if (score >= threshold) {
35
+ const entry = readEntry(index.entries[id]);
36
+ if (entry) results.push(entry);
37
+ }
38
+ }
39
+ } catch (e) {
40
+ console.error(chalk.yellow(`⚠️ Warning: Semantic search failed (${e.message}). Ensure Ollama is running.\nFallback text search will be used.`));
41
+ // Basic text fallback
42
+ const q = query.toLowerCase();
43
+ for (const id of allIds) {
44
+ const entry = readEntry(index.entries[id]);
45
+ if (!entry) continue;
46
+ const searchable = [entry.title, entry.context].join(' ').toLowerCase();
47
+ if (searchable.includes(q)) results.push(entry);
48
+ }
49
+ }
50
+
51
+ // The exact LLM-ready markdown string
52
+ const markdownPrompt = formatPromptContext(query, results);
53
+
54
+ // Print strictly to stdout without extra CLI fluff so it can be piped seamlessly
55
+ console.log(markdownPrompt);
56
+
57
+ } catch (e) {
58
+ console.error(chalk.red(`\nFailed to generate prompt: ${e.message}\n`));
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ module.exports = promptCmd;
@@ -6,9 +6,16 @@ const { readEntry } = require('../lib/entries');
6
6
  const { printEntry } = require('../lib/format');
7
7
  const { requireInit } = require('../lib/guard');
8
8
 
9
- function search(query) {
9
+ function search(queryArgs) {
10
10
  requireInit();
11
11
  try {
12
+ const query = Array.isArray(queryArgs) ? queryArgs.join(' ') : (queryArgs || '');
13
+
14
+ if (!query || query.trim().length === 0) {
15
+ console.error(chalk.red('\nError: You must provide a search query.'));
16
+ process.exit(1);
17
+ }
18
+
12
19
  const index = readIndex();
13
20
  const q = query.toLowerCase();
14
21
  const matches = [];
@@ -2,16 +2,36 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const { requireInit } = require('../lib/guard');
5
+ const { getServerStatus } = require('../mcp/status');
5
6
 
6
7
  async function serve(options) {
7
8
  requireInit();
9
+
10
+ // Prevent double-start
11
+ const existing = getServerStatus();
12
+ if (existing) {
13
+ process.stderr.write(chalk.yellow(`\n⚠ Lore MCP server is already running (PID: ${existing.pid}, started: ${existing.startedAt})\n`));
14
+ process.stderr.write(chalk.dim(` Tools: ${existing.tools.join(', ')}\n\n`));
15
+ process.exit(1);
16
+ }
17
+
8
18
  try {
19
+ // Start the MCP server (communicates via stdio)
9
20
  const { startServer } = require('../mcp/server');
10
21
  if (!options.quiet) {
11
22
  process.stderr.write(chalk.green('📖 Lore MCP server starting on stdio\n'));
12
23
  process.stderr.write(chalk.cyan(' Add to Claude Code settings: { "command": "lore serve", "args": [] }\n'));
13
24
  }
14
25
  await startServer();
26
+
27
+ // Also start the UI dashboard in the same process
28
+ const uiPort = options.port || 3333;
29
+ try {
30
+ const { startDashboard } = require('./ui');
31
+ startDashboard(uiPort);
32
+ } catch (e) {
33
+ process.stderr.write(chalk.yellow(`⚠ Could not start UI dashboard: ${e.message}\n`));
34
+ }
15
35
  } catch (e) {
16
36
  process.stderr.write(chalk.red(`Failed to start server: ${e.message}\n`));
17
37
  process.exit(1);
@@ -19,3 +39,4 @@ async function serve(options) {
19
39
  }
20
40
 
21
41
  module.exports = serve;
42
+
@@ -12,7 +12,7 @@ function stale() {
12
12
  const index = readIndex();
13
13
  let found = false;
14
14
 
15
- for (const [id, entryPath] of Object.entries(index.entries)) {
15
+ for (const entryPath of Object.values(index.entries)) {
16
16
  const entry = readEntry(entryPath);
17
17
  if (!entry) continue;
18
18
 
@@ -8,6 +8,7 @@ const { readIndex, LORE_DIR } = require('../lib/index');
8
8
  const { readEntry } = require('../lib/entries');
9
9
  const { computeScore } = require('../lib/scorer');
10
10
  const { listDrafts, acceptDraft, deleteDraft } = require('../lib/drafts');
11
+ const { getServerStatus } = require('../mcp/status');
11
12
 
12
13
  // Only load 'open' dynamically to avoid overhead on other CLI commands if not needed
13
14
  async function openBrowser(url) {
@@ -15,11 +16,9 @@ async function openBrowser(url) {
15
16
  await open(url);
16
17
  }
17
18
 
18
- function ui(options) {
19
- requireInit();
20
-
19
+ function createApp(portNum) {
21
20
  const app = express();
22
- const PORT = options.port || 3333;
21
+ const PORT = portNum || 3333;
23
22
 
24
23
  app.use(express.json());
25
24
 
@@ -56,6 +55,18 @@ function ui(options) {
56
55
  }
57
56
  });
58
57
 
58
+ app.get('/api/mcp-status', (req, res) => {
59
+ try {
60
+ const status = getServerStatus();
61
+ res.json({
62
+ active: !!status,
63
+ ...(status || {}),
64
+ });
65
+ } catch (e) {
66
+ res.json({ active: false });
67
+ }
68
+ });
69
+
59
70
  app.get('/api/entries', (req, res) => {
60
71
  try {
61
72
  const index = readIndex();
@@ -166,6 +177,23 @@ function ui(options) {
166
177
  process.exit(1);
167
178
  }
168
179
  });
180
+
181
+ return server;
182
+ }
183
+
184
+ /**
185
+ * Start just the dashboard Express server (called from serve.js).
186
+ * @param {number} port
187
+ * @returns {object} Express server instance
188
+ */
189
+ function startDashboard(port) {
190
+ return createApp(port || 3333);
191
+ }
192
+
193
+ function ui(options) {
194
+ requireInit();
195
+ createApp(options.port || 3333);
169
196
  }
170
197
 
171
198
  module.exports = ui;
199
+ module.exports.startDashboard = startDashboard;
package/src/lib/config.js CHANGED
@@ -14,6 +14,7 @@ const DEFAULTS = {
14
14
  },
15
15
  mcp: {
16
16
  tokenBudget: 4000,
17
+ confirmEntries: true,
17
18
  },
18
19
  };
19
20
 
package/src/lib/drafts.js CHANGED
@@ -44,41 +44,82 @@ function listDrafts() {
44
44
  }
45
45
 
46
46
  /**
47
- * Promote a draft to a real Lore entry.
47
+ * Promote a draft to a real Lore entry, or apply it as an update to an existing entry.
48
48
  * @param {string} draftId
49
- * @returns {object} the created entry
49
+ * @returns {object} the created or updated entry
50
50
  */
51
51
  function acceptDraft(draftId) {
52
52
  const draftPath = path.join(DRAFTS_DIR(), `${draftId}.json`);
53
53
  const draft = fs.readJsonSync(draftPath);
54
54
 
55
- const type = draft.suggestedType || 'decision';
56
- const title = draft.suggestedTitle || 'Untitled';
57
- const id = generateId(type, title);
58
-
59
- const entry = {
60
- id,
61
- type,
62
- title,
63
- context: draft.evidence || '',
64
- files: draft.files || [],
65
- tags: draft.tags || [],
66
- alternatives: [],
67
- tradeoffs: '',
68
- date: new Date().toISOString(),
69
- };
70
-
71
- writeEntry(entry);
72
- const index = readIndex();
73
- addEntryToIndex(index, entry);
74
- writeIndex(index);
75
-
76
- // Auto-embed if Ollama available
77
- try {
78
- const { generateEmbedding, storeEmbedding } = require('./embeddings');
79
- const text = [title, entry.context, ...entry.tags].join(' ');
80
- generateEmbedding(text).then(vec => storeEmbedding(id, vec)).catch(() => {});
81
- } catch (e) {}
55
+ let entry;
56
+
57
+ if (draft.updatesEntryId) {
58
+ // This draft is an update to an existing entry — modify in place
59
+ const index = readIndex();
60
+ const existingPath = index.entries[draft.updatesEntryId];
61
+ if (!existingPath) {
62
+ throw new Error(`Original entry "${draft.updatesEntryId}" not found. It may have been deleted.`);
63
+ }
64
+
65
+ const { readEntry: readExisting } = require('./entries');
66
+ entry = readExisting(existingPath);
67
+ if (!entry) {
68
+ throw new Error(`Failed to read original entry "${draft.updatesEntryId}".`);
69
+ }
70
+
71
+ // Apply updates from draft — only override fields that have meaningful values
72
+ if (draft.suggestedTitle && draft.suggestedTitle !== entry.title) entry.title = draft.suggestedTitle;
73
+ if (draft.evidence && !draft.evidence.startsWith('[UPDATE to ')) entry.context = draft.evidence;
74
+ else if (draft.evidence) {
75
+ // Strip the "[UPDATE to xxx] " prefix from the context
76
+ entry.context = draft.evidence.replace(/^\[UPDATE to [^\]]+\]\s*/, '');
77
+ }
78
+ if (draft.files && draft.files.length > 0) entry.files = draft.files;
79
+ if (draft.tags && draft.tags.length > 0) entry.tags = draft.tags;
80
+ if (draft.alternatives && draft.alternatives.length > 0) entry.alternatives = draft.alternatives;
81
+ if (draft.tradeoffs) entry.tradeoffs = draft.tradeoffs;
82
+ entry.lastUpdated = new Date().toISOString();
83
+
84
+ writeEntry(entry);
85
+ writeIndex(index);
86
+
87
+ // Re-embed
88
+ try {
89
+ const { generateEmbedding, storeEmbedding } = require('./embeddings');
90
+ const text = [entry.title, entry.context, ...(entry.alternatives || []), entry.tradeoffs || '', ...(entry.tags || [])].join(' ');
91
+ generateEmbedding(text).then(vec => storeEmbedding(entry.id, vec)).catch(() => {});
92
+ } catch (e) {}
93
+ } else {
94
+ // Standard draft → new entry
95
+ const type = draft.suggestedType || 'decision';
96
+ const title = draft.suggestedTitle || 'Untitled';
97
+ const id = generateId(type, title);
98
+
99
+ entry = {
100
+ id,
101
+ type,
102
+ title,
103
+ context: draft.evidence || '',
104
+ files: draft.files || [],
105
+ tags: draft.tags || [],
106
+ alternatives: draft.alternatives || [],
107
+ tradeoffs: draft.tradeoffs || '',
108
+ date: new Date().toISOString(),
109
+ };
110
+
111
+ writeEntry(entry);
112
+ const index = readIndex();
113
+ addEntryToIndex(index, entry);
114
+ writeIndex(index);
115
+
116
+ // Auto-embed if Ollama available
117
+ try {
118
+ const { generateEmbedding, storeEmbedding } = require('./embeddings');
119
+ const text = [title, entry.context, ...entry.alternatives, entry.tradeoffs, ...entry.tags].join(' ');
120
+ generateEmbedding(text).then(vec => storeEmbedding(id, vec)).catch(() => {});
121
+ } catch (e) {}
122
+ }
82
123
 
83
124
  fs.removeSync(draftPath);
84
125
  return entry;
@@ -50,10 +50,71 @@ function readAllEntries(index) {
50
50
  return entries;
51
51
  }
52
52
 
53
+ /**
54
+ * Check if a similar entry or draft already exists.
55
+ * @param {object} index - The lore index
56
+ * @param {string} type - Entry type
57
+ * @param {string} title - Entry title
58
+ * @returns {{ match: 'exact'|'fuzzy', entry: object, source: 'entry'|'draft' }|null}
59
+ */
60
+ function findDuplicate(index, type, title) {
61
+ const normalizedTitle = title.toLowerCase().trim();
62
+ const titleWords = new Set(normalizedTitle.split(/\s+/).filter(w => w.length > 2));
63
+
64
+ function checkTitle(candidateTitle) {
65
+ const candidate = (candidateTitle || '').toLowerCase().trim();
66
+
67
+ // Exact match (case-insensitive)
68
+ if (candidate === normalizedTitle) return 'exact';
69
+
70
+ // Fuzzy match: ≥60% word overlap
71
+ if (titleWords.size > 0) {
72
+ const candidateWords = new Set(candidate.split(/\s+/).filter(w => w.length > 2));
73
+ if (candidateWords.size === 0) return null;
74
+
75
+ let overlap = 0;
76
+ for (const w of titleWords) {
77
+ if (candidateWords.has(w)) overlap++;
78
+ }
79
+
80
+ const similarity = overlap / Math.max(titleWords.size, candidateWords.size);
81
+ if (similarity >= 0.6) return 'fuzzy';
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ // Check approved entries
88
+ for (const entryPath of Object.values(index.entries)) {
89
+ const entry = readEntry(entryPath);
90
+ if (!entry || entry.type !== type) continue;
91
+
92
+ const match = checkTitle(entry.title);
93
+ if (match) return { match, entry, source: 'entry' };
94
+ }
95
+
96
+ // Check pending drafts
97
+ try {
98
+ const { listDrafts } = require('./drafts');
99
+ const drafts = listDrafts();
100
+ for (const draft of drafts) {
101
+ if (draft.suggestedType !== type) continue;
102
+
103
+ const match = checkTitle(draft.suggestedTitle);
104
+ if (match) return { match, entry: { id: draft.draftId, title: draft.suggestedTitle, type: draft.suggestedType }, source: 'draft' };
105
+ }
106
+ } catch (e) {
107
+ // drafts module not available — skip
108
+ }
109
+
110
+ return null;
111
+ }
112
+
53
113
  module.exports = {
54
114
  getEntryPath,
55
115
  readEntry,
56
116
  writeEntry,
57
117
  generateId,
58
118
  readAllEntries,
119
+ findDuplicate,
59
120
  };
package/src/lib/format.js CHANGED
@@ -41,7 +41,7 @@ function printEntry(entry) {
41
41
  content += `${titleColor(typeLabel)} ${titleColor(entry.title)} ${chalk.gray('(' + entry.date + ')')}\n\n`;
42
42
 
43
43
  // Wrap context text roughly
44
- const contextWords = entry.context.split(' ');
44
+ const contextWords = (entry.context || '').split(' ');
45
45
  let currentLine = '';
46
46
  for (const word of contextWords) {
47
47
  if (currentLine.length + word.length > 70) {
@@ -71,4 +71,47 @@ function printEntry(entry) {
71
71
  drawBox(content.trim(), colorFn, entry.id);
72
72
  }
73
73
 
74
- module.exports = { printEntry };
74
+ function formatPromptContext(query, entries) {
75
+ if (!entries || entries.length === 0) {
76
+ return `# Project Context: Lore\nThe user is asking about: "${query}"\n\nNo specific historical rules or decisions were found in the project memory for this topic.`;
77
+ }
78
+
79
+ const invariants = entries.filter(e => e.type === 'invariant');
80
+ const gotchas = entries.filter(e => e.type === 'gotcha');
81
+ const decisions = entries.filter(e => e.type === 'decision');
82
+ const graveyard = entries.filter(e => e.type === 'graveyard');
83
+
84
+ let output = `# Project Context: Lore\nThe user is asking: "${query}"\n\nPlease adhere strictly to the following project architectural rules extracted from project memory:\n`;
85
+
86
+ if (invariants.length > 0) {
87
+ output += `\n## Invariants (CRITICAL: DO NOT BREAK)\n`;
88
+ for (const e of invariants) {
89
+ output += `- [${e.id}]: ${(e.context || '').trim().replace(/\n/g, ' ')}\n`;
90
+ }
91
+ }
92
+
93
+ if (gotchas.length > 0) {
94
+ output += `\n## Gotchas (WARNING: KNOWN TRAPS)\n`;
95
+ for (const e of gotchas) {
96
+ output += `- [${e.id}]: ${(e.context || '').trim().replace(/\n/g, ' ')}\n`;
97
+ }
98
+ }
99
+
100
+ if (decisions.length > 0) {
101
+ output += `\n## Relevant Decisions\n`;
102
+ for (const e of decisions) {
103
+ output += `- [${e.id}]: ${e.title} - ${(e.context || '').trim().replace(/\n/g, ' ')}\n`;
104
+ }
105
+ }
106
+
107
+ if (graveyard.length > 0) {
108
+ output += `\n## Graveyard (DO NOT SUGGEST OR USE)\n`;
109
+ for (const e of graveyard) {
110
+ output += `- [${e.id}]: ${(e.context || '').trim().replace(/\n/g, ' ')}\n`;
111
+ }
112
+ }
113
+
114
+ return output.trim();
115
+ }
116
+
117
+ module.exports = { printEntry, formatPromptContext };
@@ -21,19 +21,21 @@ function scoreEntry(entry, context) {
21
21
  let score = 0;
22
22
 
23
23
  // Direct file match
24
+ let exactMatchFound = false;
24
25
  if (context.filepath && entry.files && entry.files.length > 0) {
25
26
  const normalizedQuery = context.filepath.replace(/^\.\//, '');
26
27
  for (const f of entry.files) {
27
28
  const normalizedFile = f.replace(/^\.\//, '');
28
29
  if (normalizedFile === normalizedQuery) {
29
30
  score += WEIGHTS.directFileMatch;
31
+ exactMatchFound = true;
30
32
  break;
31
33
  }
32
34
  }
33
35
  }
34
36
 
35
37
  // Parent dir match
36
- if (context.filepath && entry.files && entry.files.length > 0) {
38
+ if (!exactMatchFound && context.filepath && entry.files && entry.files.length > 0) {
37
39
  const queryDir = path.dirname(context.filepath.replace(/^\.\//, ''));
38
40
  for (const f of entry.files) {
39
41
  const fileDir = path.dirname(f.replace(/^\.\//, ''));