Skip to content

Commit a6b864e

Browse files
feat(discovery): Add discovery CLI and MCP tools
- Add discovery CLI commands for finding relevant files - Add MCP discovery handlers for context-aware file discovery - Fix claude-sm dotenv loading (override stale shell env vars)
1 parent 8a43b07 commit a6b864e

8 files changed

Lines changed: 1575 additions & 37 deletions

File tree

src/cli/claude-sm.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
* Automatically manages context persistence and instance isolation
66
*/
77

8+
import { config as loadDotenv } from 'dotenv';
9+
loadDotenv({ override: true });
10+
811
import { spawn, execSync, execFileSync } from 'child_process';
912
import * as fs from 'fs';
1013
import * as path from 'path';

src/cli/commands/discovery.ts

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
/**
2+
* Discovery CLI Commands
3+
* Discover relevant files based on current context
4+
*/
5+
6+
import { Command } from 'commander';
7+
import Database from 'better-sqlite3';
8+
import { join } from 'path';
9+
import { existsSync } from 'fs';
10+
import chalk from 'chalk';
11+
import { FrameManager } from '../../core/context/frame-manager.js';
12+
import { LLMContextRetrieval } from '../../core/retrieval/index.js';
13+
import { DiscoveryHandlers } from '../../integrations/mcp/handlers/discovery-handlers.js';
14+
15+
export function createDiscoveryCommands(): Command {
16+
const discovery = new Command('discovery')
17+
.alias('discover')
18+
.description('Discover relevant files based on current context');
19+
20+
// Main discovery command
21+
discovery
22+
.command('files')
23+
.alias('f')
24+
.description('Discover files relevant to current context')
25+
.option('-q, --query <text>', 'Focus discovery on specific query')
26+
.option(
27+
'-d, --depth <level>',
28+
'Search depth: shallow, medium, deep',
29+
'medium'
30+
)
31+
.option('-m, --max <n>', 'Maximum files to return', '20')
32+
.option('-i, --include <patterns>', 'Include patterns (comma-separated)')
33+
.option('-e, --exclude <patterns>', 'Exclude patterns (comma-separated)')
34+
.action(async (options) => {
35+
const projectRoot = process.cwd();
36+
const dbPath = join(projectRoot, '.stackmemory', 'context.db');
37+
38+
if (!existsSync(dbPath)) {
39+
console.log(
40+
chalk.red('StackMemory not initialized in this directory.')
41+
);
42+
console.log(chalk.gray('Run "stackmemory init" first.'));
43+
return;
44+
}
45+
46+
const db = new Database(dbPath);
47+
48+
try {
49+
let projectId = 'default';
50+
try {
51+
const row = db
52+
.prepare(`SELECT value FROM metadata WHERE key = 'project_id'`)
53+
.get() as any;
54+
if (row?.value) projectId = row.value;
55+
} catch {}
56+
57+
const frameManager = new FrameManager(db, projectId, {
58+
skipContextBridge: true,
59+
});
60+
const contextRetrieval = new LLMContextRetrieval(
61+
db,
62+
frameManager,
63+
projectId
64+
);
65+
const handlers = new DiscoveryHandlers({
66+
frameManager,
67+
contextRetrieval,
68+
db,
69+
projectRoot,
70+
});
71+
72+
console.log(chalk.blue('\nDiscovering relevant files...\n'));
73+
74+
const result = await handlers.handleDiscover({
75+
query: options.query,
76+
depth: options.depth as 'shallow' | 'medium' | 'deep',
77+
maxFiles: parseInt(options.max),
78+
includePatterns: options.include?.split(','),
79+
excludePatterns: options.exclude?.split(','),
80+
});
81+
82+
// Display results
83+
const metadata = result.metadata;
84+
85+
console.log(chalk.cyan('Context Summary'));
86+
console.log(chalk.gray(metadata.contextSummary));
87+
console.log('');
88+
89+
console.log(chalk.cyan('Keywords Extracted'));
90+
console.log(chalk.gray(metadata.keywords.slice(0, 15).join(', ')));
91+
console.log('');
92+
93+
console.log(chalk.cyan('Relevant Files'));
94+
for (const file of metadata.files.slice(0, parseInt(options.max))) {
95+
const icon =
96+
file.relevance === 'high'
97+
? chalk.green('[HIGH]')
98+
: file.relevance === 'medium'
99+
? chalk.yellow('[MED]')
100+
: chalk.gray('[LOW]');
101+
console.log(`${icon} ${chalk.white(file.path)}`);
102+
console.log(chalk.gray(` ${file.reason}`));
103+
}
104+
105+
if (Object.keys(metadata.mdContext).length > 0) {
106+
console.log('');
107+
console.log(chalk.cyan('MD Files Parsed'));
108+
for (const mdFile of Object.keys(metadata.mdContext)) {
109+
console.log(chalk.gray(` - ${mdFile}`));
110+
}
111+
}
112+
113+
console.log('');
114+
} finally {
115+
db.close();
116+
}
117+
});
118+
119+
// Related files command
120+
discovery
121+
.command('related')
122+
.alias('r')
123+
.description('Find files related to a specific file or concept')
124+
.option('-f, --file <path>', 'File to find related files for')
125+
.option('-c, --concept <text>', 'Concept to search for')
126+
.option('-m, --max <n>', 'Maximum files to return', '10')
127+
.action(async (options) => {
128+
if (!options.file && !options.concept) {
129+
console.log(chalk.red('Either --file or --concept is required'));
130+
return;
131+
}
132+
133+
const projectRoot = process.cwd();
134+
const dbPath = join(projectRoot, '.stackmemory', 'context.db');
135+
136+
if (!existsSync(dbPath)) {
137+
console.log(
138+
chalk.red('StackMemory not initialized in this directory.')
139+
);
140+
return;
141+
}
142+
143+
const db = new Database(dbPath);
144+
145+
try {
146+
let projectId = 'default';
147+
try {
148+
const row = db
149+
.prepare(`SELECT value FROM metadata WHERE key = 'project_id'`)
150+
.get() as any;
151+
if (row?.value) projectId = row.value;
152+
} catch {}
153+
154+
const frameManager = new FrameManager(db, projectId, {
155+
skipContextBridge: true,
156+
});
157+
const contextRetrieval = new LLMContextRetrieval(
158+
db,
159+
frameManager,
160+
projectId
161+
);
162+
const handlers = new DiscoveryHandlers({
163+
frameManager,
164+
contextRetrieval,
165+
db,
166+
projectRoot,
167+
});
168+
169+
const target = options.file || options.concept;
170+
console.log(chalk.blue(`\nFinding files related to: ${target}\n`));
171+
172+
const result = await handlers.handleRelatedFiles({
173+
file: options.file,
174+
concept: options.concept,
175+
maxFiles: parseInt(options.max),
176+
});
177+
178+
const files = result.metadata.relatedFiles;
179+
180+
if (files.length === 0) {
181+
console.log(chalk.gray('No related files found.'));
182+
return;
183+
}
184+
185+
for (const file of files) {
186+
const icon =
187+
file.relevance === 'high'
188+
? chalk.green('[HIGH]')
189+
: chalk.yellow('[MED]');
190+
console.log(`${icon} ${chalk.white(file.path)}`);
191+
console.log(chalk.gray(` ${file.reason}`));
192+
}
193+
194+
console.log('');
195+
} finally {
196+
db.close();
197+
}
198+
});
199+
200+
// Session summary command
201+
discovery
202+
.command('session')
203+
.alias('s')
204+
.description('Get current session summary')
205+
.option('--no-files', 'Exclude recent files')
206+
.option('--no-decisions', 'Exclude recent decisions')
207+
.action(async (options) => {
208+
const projectRoot = process.cwd();
209+
const dbPath = join(projectRoot, '.stackmemory', 'context.db');
210+
211+
if (!existsSync(dbPath)) {
212+
console.log(
213+
chalk.red('StackMemory not initialized in this directory.')
214+
);
215+
return;
216+
}
217+
218+
const db = new Database(dbPath);
219+
220+
try {
221+
let projectId = 'default';
222+
try {
223+
const row = db
224+
.prepare(`SELECT value FROM metadata WHERE key = 'project_id'`)
225+
.get() as any;
226+
if (row?.value) projectId = row.value;
227+
} catch {}
228+
229+
const frameManager = new FrameManager(db, projectId, {
230+
skipContextBridge: true,
231+
});
232+
const contextRetrieval = new LLMContextRetrieval(
233+
db,
234+
frameManager,
235+
projectId
236+
);
237+
const handlers = new DiscoveryHandlers({
238+
frameManager,
239+
contextRetrieval,
240+
db,
241+
projectRoot,
242+
});
243+
244+
const result = await handlers.handleSessionSummary({
245+
includeFiles: options.files !== false,
246+
includeDecisions: options.decisions !== false,
247+
});
248+
249+
const summary = result.metadata;
250+
251+
console.log(chalk.blue('\nSession Summary\n'));
252+
253+
console.log(`${chalk.cyan('Current Goal:')} ${summary.currentGoal}`);
254+
console.log(`${chalk.cyan('Active Frames:')} ${summary.activeFrames}`);
255+
console.log(`${chalk.cyan('Stack Depth:')} ${summary.stackDepth}`);
256+
257+
if (summary.recentFiles?.length > 0) {
258+
console.log(chalk.cyan('\nRecent Files:'));
259+
for (const f of summary.recentFiles.slice(0, 10)) {
260+
console.log(chalk.gray(` - ${f}`));
261+
}
262+
}
263+
264+
if (summary.decisions?.length > 0) {
265+
console.log(chalk.cyan('\nRecent Decisions:'));
266+
for (const d of summary.decisions.slice(0, 5)) {
267+
console.log(
268+
chalk.gray(
269+
` [${d.type}] ${d.text.slice(0, 60)}${d.text.length > 60 ? '...' : ''}`
270+
)
271+
);
272+
}
273+
}
274+
275+
console.log('');
276+
} finally {
277+
db.close();
278+
}
279+
});
280+
281+
// Quick context command (default)
282+
discovery
283+
.command('quick', { isDefault: true })
284+
.description('Quick discovery based on current context')
285+
.action(async () => {
286+
const projectRoot = process.cwd();
287+
const dbPath = join(projectRoot, '.stackmemory', 'context.db');
288+
289+
if (!existsSync(dbPath)) {
290+
console.log(
291+
chalk.red('StackMemory not initialized in this directory.')
292+
);
293+
console.log(chalk.gray('Run "stackmemory init" first.'));
294+
return;
295+
}
296+
297+
const db = new Database(dbPath);
298+
299+
try {
300+
let projectId = 'default';
301+
try {
302+
const row = db
303+
.prepare(`SELECT value FROM metadata WHERE key = 'project_id'`)
304+
.get() as any;
305+
if (row?.value) projectId = row.value;
306+
} catch {}
307+
308+
const frameManager = new FrameManager(db, projectId, {
309+
skipContextBridge: true,
310+
});
311+
const contextRetrieval = new LLMContextRetrieval(
312+
db,
313+
frameManager,
314+
projectId
315+
);
316+
const handlers = new DiscoveryHandlers({
317+
frameManager,
318+
contextRetrieval,
319+
db,
320+
projectRoot,
321+
});
322+
323+
console.log(chalk.blue('\nQuick Discovery\n'));
324+
325+
// Get session summary first
326+
const sessionResult = await handlers.handleSessionSummary({
327+
includeFiles: true,
328+
includeDecisions: true,
329+
});
330+
const session = sessionResult.metadata;
331+
332+
console.log(`${chalk.cyan('Current:')} ${session.currentGoal}`);
333+
console.log(`${chalk.cyan('Stack:')} ${session.stackDepth} frames`);
334+
console.log('');
335+
336+
// Quick file discovery
337+
const discoverResult = await handlers.handleDiscover({
338+
depth: 'shallow',
339+
maxFiles: 10,
340+
});
341+
const discovery = discoverResult.metadata;
342+
343+
console.log(
344+
chalk.cyan('Keywords: ') +
345+
chalk.gray(discovery.keywords.slice(0, 8).join(', '))
346+
);
347+
console.log('');
348+
349+
console.log(chalk.cyan('Top Relevant Files:'));
350+
for (const file of discovery.files.slice(0, 5)) {
351+
const icon =
352+
file.relevance === 'high' ? chalk.green('*') : chalk.yellow('-');
353+
console.log(`${icon} ${file.path}`);
354+
}
355+
356+
if (session.decisions?.length > 0) {
357+
console.log('');
358+
console.log(chalk.cyan('Recent Decisions:'));
359+
for (const d of session.decisions.slice(0, 3)) {
360+
console.log(chalk.gray(` [${d.type}] ${d.text.slice(0, 50)}...`));
361+
}
362+
}
363+
364+
console.log('');
365+
console.log(
366+
chalk.gray('Use "stackmemory discovery files" for detailed discovery')
367+
);
368+
} finally {
369+
db.close();
370+
}
371+
});
372+
373+
return discovery;
374+
}

0 commit comments

Comments
 (0)