mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: Add CLAUDE.md freshness tracking and update reminders
- Add SQLite table and CRUD methods for tracking update history - Create freshness calculation service based on git file changes - Add API endpoints for freshness data, marking updates, and history - Display freshness badges in file tree (green/yellow/red indicators) - Show freshness gauge and details in metadata panel - Auto-mark files as updated after CLI sync - Add English and Chinese i18n translations Freshness algorithm: 100 - min((changedFilesCount / 20) * 100, 100) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
319
ccw/src/core/claude-freshness.ts
Normal file
319
ccw/src/core/claude-freshness.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* CLAUDE.md Freshness Calculator
|
||||
* Calculates freshness scores based on git changes since last update
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync, statSync, readdirSync } from 'fs';
|
||||
import { dirname, extname, relative, join } from 'path';
|
||||
import { getCoreMemoryStore, ClaudeUpdateRecord } from './core-memory-store.js';
|
||||
|
||||
// Source file extensions to track (from detect-changed-modules.ts)
|
||||
const SOURCE_EXTENSIONS = [
|
||||
'.md', '.js', '.ts', '.jsx', '.tsx',
|
||||
'.py', '.go', '.rs', '.java', '.cpp', '.c', '.h',
|
||||
'.sh', '.ps1', '.json', '.yaml', '.yml'
|
||||
];
|
||||
|
||||
// Directories to exclude
|
||||
const EXCLUDE_DIRS = [
|
||||
'.git', '__pycache__', 'node_modules', '.venv', 'venv', 'env',
|
||||
'dist', 'build', '.cache', '.pytest_cache', '.mypy_cache',
|
||||
'coverage', '.nyc_output', 'logs', 'tmp', 'temp', '.ccw', '.workflow'
|
||||
];
|
||||
|
||||
export interface FreshnessResult {
|
||||
path: string;
|
||||
level: 'user' | 'project' | 'module';
|
||||
relativePath: string;
|
||||
parentDirectory?: string;
|
||||
lastUpdated: string | null;
|
||||
lastModified: string;
|
||||
changedFilesCount: number;
|
||||
freshness: number;
|
||||
updateSource?: string;
|
||||
needsUpdate: boolean;
|
||||
changedFiles?: string[];
|
||||
}
|
||||
|
||||
export interface FreshnessSummary {
|
||||
totalFiles: number;
|
||||
staleCount: number;
|
||||
averageFreshness: number;
|
||||
lastScanAt: string;
|
||||
}
|
||||
|
||||
export interface FreshnessResponse {
|
||||
files: FreshnessResult[];
|
||||
summary: FreshnessSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if git is available and we're in a repo
|
||||
*/
|
||||
function isGitRepo(basePath: string): boolean {
|
||||
try {
|
||||
execSync('git rev-parse --git-dir', { cwd: basePath, stdio: 'pipe' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current git commit hash
|
||||
*/
|
||||
export function getCurrentGitCommit(basePath: string): string | null {
|
||||
try {
|
||||
const output = execSync('git rev-parse HEAD', {
|
||||
cwd: basePath,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
}).trim();
|
||||
return output || null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files changed since a specific date within a directory
|
||||
*/
|
||||
function getChangedFilesSince(basePath: string, modulePath: string, sinceDate: string): string[] {
|
||||
try {
|
||||
// Format date for git
|
||||
const date = new Date(sinceDate);
|
||||
const formattedDate = date.toISOString().split('T')[0];
|
||||
|
||||
// Get files changed since the date
|
||||
const output = execSync(
|
||||
`git log --name-only --since="${formattedDate}" --pretty=format: -- "${modulePath}"`,
|
||||
{
|
||||
cwd: basePath,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
}
|
||||
).trim();
|
||||
|
||||
if (!output) return [];
|
||||
|
||||
// Get unique files and filter by source extensions
|
||||
const files = [...new Set(output.split('\n').filter(f => f.trim()))];
|
||||
return files.filter(f => {
|
||||
const ext = extname(f).toLowerCase();
|
||||
return SOURCE_EXTENSIONS.includes(ext);
|
||||
});
|
||||
} catch (e) {
|
||||
// Fallback to mtime-based detection
|
||||
return findFilesModifiedSince(modulePath, sinceDate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: Find files modified since a date using mtime
|
||||
*/
|
||||
function findFilesModifiedSince(dirPath: string, sinceDate: string): string[] {
|
||||
const results: string[] = [];
|
||||
const cutoffTime = new Date(sinceDate).getTime();
|
||||
|
||||
function scan(currentPath: string): void {
|
||||
try {
|
||||
const entries = readdirSync(currentPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (EXCLUDE_DIRS.includes(entry.name)) continue;
|
||||
scan(join(currentPath, entry.name));
|
||||
} else if (entry.isFile()) {
|
||||
const ext = extname(entry.name).toLowerCase();
|
||||
if (!SOURCE_EXTENSIONS.includes(ext)) continue;
|
||||
|
||||
const fullPath = join(currentPath, entry.name);
|
||||
try {
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.mtimeMs > cutoffTime) {
|
||||
results.push(relative(dirPath, fullPath));
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip files we can't stat
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore permission errors
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(dirPath)) {
|
||||
scan(dirPath);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate freshness for a single CLAUDE.md file
|
||||
*/
|
||||
export function calculateFreshness(
|
||||
filePath: string,
|
||||
fileLevel: 'user' | 'project' | 'module',
|
||||
lastUpdateTime: string | null,
|
||||
lastModified: string,
|
||||
projectPath: string,
|
||||
threshold: number = 20
|
||||
): FreshnessResult {
|
||||
// Use lastUpdateTime from history, or fall back to file mtime
|
||||
const effectiveUpdateTime = lastUpdateTime || lastModified;
|
||||
|
||||
// Calculate module path for change detection
|
||||
let modulePath: string | null = null;
|
||||
let changedFiles: string[] = [];
|
||||
|
||||
if (fileLevel === 'module') {
|
||||
// For module-level files, scan the parent directory
|
||||
modulePath = dirname(filePath);
|
||||
} else if (fileLevel === 'project') {
|
||||
// For project-level files, scan the project root
|
||||
modulePath = projectPath;
|
||||
}
|
||||
|
||||
// Only calculate changes for module/project level in git repos
|
||||
if (modulePath && isGitRepo(projectPath)) {
|
||||
changedFiles = getChangedFilesSince(projectPath, modulePath, effectiveUpdateTime);
|
||||
// Exclude the CLAUDE.md file itself
|
||||
changedFiles = changedFiles.filter(f => !f.endsWith('CLAUDE.md'));
|
||||
}
|
||||
|
||||
// Calculate freshness percentage
|
||||
const changedCount = changedFiles.length;
|
||||
const freshness = Math.max(0, 100 - Math.floor((changedCount / threshold) * 100));
|
||||
|
||||
// Determine parent directory for display
|
||||
const parentDirectory = fileLevel === 'module'
|
||||
? filePath.split(/[\\/]/).slice(-2, -1)[0]
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
level: fileLevel,
|
||||
relativePath: relative(projectPath, filePath).replace(/\\/g, '/'),
|
||||
parentDirectory,
|
||||
lastUpdated: lastUpdateTime,
|
||||
lastModified,
|
||||
changedFilesCount: changedCount,
|
||||
freshness,
|
||||
needsUpdate: freshness < 50,
|
||||
changedFiles: changedFiles.slice(0, 20) // Limit to first 20 for detail view
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate freshness for all CLAUDE.md files in a project
|
||||
*/
|
||||
export function calculateAllFreshness(
|
||||
claudeFiles: Array<{
|
||||
path: string;
|
||||
level: 'user' | 'project' | 'module';
|
||||
lastModified: string;
|
||||
}>,
|
||||
projectPath: string,
|
||||
threshold: number = 20
|
||||
): FreshnessResponse {
|
||||
// Get update records from store
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const updateRecords = store.getAllClaudeUpdateRecords();
|
||||
|
||||
// Create a map for quick lookup
|
||||
const updateMap = new Map<string, ClaudeUpdateRecord>();
|
||||
for (const record of updateRecords) {
|
||||
updateMap.set(record.file_path, record);
|
||||
}
|
||||
|
||||
const results: FreshnessResult[] = [];
|
||||
|
||||
for (const file of claudeFiles) {
|
||||
const updateRecord = updateMap.get(file.path);
|
||||
|
||||
const result = calculateFreshness(
|
||||
file.path,
|
||||
file.level,
|
||||
updateRecord?.updated_at || null,
|
||||
file.lastModified,
|
||||
projectPath,
|
||||
threshold
|
||||
);
|
||||
|
||||
if (updateRecord) {
|
||||
result.updateSource = updateRecord.update_source;
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Calculate summary
|
||||
const staleCount = results.filter(r => r.needsUpdate).length;
|
||||
const totalFreshness = results.reduce((sum, r) => sum + r.freshness, 0);
|
||||
const averageFreshness = results.length > 0 ? Math.round(totalFreshness / results.length) : 100;
|
||||
|
||||
return {
|
||||
files: results,
|
||||
summary: {
|
||||
totalFiles: results.length,
|
||||
staleCount,
|
||||
averageFreshness,
|
||||
lastScanAt: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a CLAUDE.md file as updated
|
||||
*/
|
||||
export function markFileAsUpdated(
|
||||
filePath: string,
|
||||
fileLevel: 'user' | 'project' | 'module',
|
||||
updateSource: 'manual' | 'cli_sync' | 'dashboard' | 'api',
|
||||
projectPath: string,
|
||||
metadata?: object
|
||||
): ClaudeUpdateRecord {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Get current git commit
|
||||
const gitCommit = getCurrentGitCommit(projectPath);
|
||||
|
||||
// Calculate changed files count before this update
|
||||
const lastUpdate = store.getLastClaudeUpdate(filePath);
|
||||
let filesChangedCount = 0;
|
||||
|
||||
if (lastUpdate && isGitRepo(projectPath)) {
|
||||
const modulePath = fileLevel === 'module' ? dirname(filePath) : projectPath;
|
||||
const changedFiles = getChangedFilesSince(projectPath, modulePath, lastUpdate.updated_at);
|
||||
filesChangedCount = changedFiles.filter(f => !f.endsWith('CLAUDE.md')).length;
|
||||
}
|
||||
|
||||
// Insert update record
|
||||
const record = store.insertClaudeUpdateRecord({
|
||||
file_path: filePath,
|
||||
file_level: fileLevel,
|
||||
module_path: fileLevel === 'module' ? dirname(filePath) : undefined,
|
||||
updated_at: now,
|
||||
update_source: updateSource,
|
||||
git_commit_hash: gitCommit || undefined,
|
||||
files_changed_before_update: filesChangedCount,
|
||||
metadata: metadata ? JSON.stringify(metadata) : undefined
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update history for a file
|
||||
*/
|
||||
export function getUpdateHistory(
|
||||
filePath: string,
|
||||
projectPath: string,
|
||||
limit: number = 50
|
||||
): ClaudeUpdateRecord[] {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
return store.getClaudeUpdateHistory(filePath, limit);
|
||||
}
|
||||
@@ -71,6 +71,18 @@ export interface MemoryChunk {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ClaudeUpdateRecord {
|
||||
id?: number;
|
||||
file_path: string;
|
||||
file_level: 'user' | 'project' | 'module';
|
||||
module_path?: string;
|
||||
updated_at: string;
|
||||
update_source: 'manual' | 'cli_sync' | 'dashboard' | 'api';
|
||||
git_commit_hash?: string;
|
||||
files_changed_before_update: number;
|
||||
metadata?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core Memory Store using SQLite
|
||||
*/
|
||||
@@ -176,6 +188,20 @@ export class CoreMemoryStore {
|
||||
UNIQUE(source_id, chunk_index)
|
||||
);
|
||||
|
||||
-- CLAUDE.md update history table
|
||||
CREATE TABLE IF NOT EXISTS claude_update_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_path TEXT NOT NULL,
|
||||
file_level TEXT NOT NULL CHECK(file_level IN ('user', 'project', 'module')),
|
||||
module_path TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
update_source TEXT NOT NULL CHECK(update_source IN ('manual', 'cli_sync', 'dashboard', 'api')),
|
||||
git_commit_hash TEXT,
|
||||
files_changed_before_update INTEGER DEFAULT 0,
|
||||
metadata TEXT,
|
||||
UNIQUE(file_path, updated_at)
|
||||
);
|
||||
|
||||
-- Indexes for efficient queries
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at DESC);
|
||||
@@ -186,6 +212,9 @@ export class CoreMemoryStore {
|
||||
CREATE INDEX IF NOT EXISTS idx_session_metadata_type ON session_metadata_cache(session_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_chunks_source ON memory_chunks(source_id, source_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_chunks_embedded ON memory_chunks(embedding IS NOT NULL);
|
||||
CREATE INDEX IF NOT EXISTS idx_claude_history_path ON claude_update_history(file_path);
|
||||
CREATE INDEX IF NOT EXISTS idx_claude_history_updated ON claude_update_history(updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_claude_history_module ON claude_update_history(module_path);
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -1078,6 +1107,128 @@ ${memory.content}
|
||||
stmt.run(sourceId);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CLAUDE.md Update History CRUD Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Insert a CLAUDE.md update record
|
||||
*/
|
||||
insertClaudeUpdateRecord(record: Omit<ClaudeUpdateRecord, 'id'>): ClaudeUpdateRecord {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO claude_update_history
|
||||
(file_path, file_level, module_path, updated_at, update_source, git_commit_hash, files_changed_before_update, metadata)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
record.file_path,
|
||||
record.file_level,
|
||||
record.module_path || null,
|
||||
record.updated_at,
|
||||
record.update_source,
|
||||
record.git_commit_hash || null,
|
||||
record.files_changed_before_update,
|
||||
record.metadata || null
|
||||
);
|
||||
|
||||
return {
|
||||
id: result.lastInsertRowid as number,
|
||||
...record
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last update record for a file
|
||||
*/
|
||||
getLastClaudeUpdate(filePath: string): ClaudeUpdateRecord | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM claude_update_history
|
||||
WHERE file_path = ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
const row = stmt.get(filePath) as any;
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
file_path: row.file_path,
|
||||
file_level: row.file_level,
|
||||
module_path: row.module_path,
|
||||
updated_at: row.updated_at,
|
||||
update_source: row.update_source,
|
||||
git_commit_hash: row.git_commit_hash,
|
||||
files_changed_before_update: row.files_changed_before_update,
|
||||
metadata: row.metadata
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update history for a file
|
||||
*/
|
||||
getClaudeUpdateHistory(filePath: string, limit: number = 50): ClaudeUpdateRecord[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM claude_update_history
|
||||
WHERE file_path = ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
const rows = stmt.all(filePath, limit) as any[];
|
||||
return rows.map(row => ({
|
||||
id: row.id,
|
||||
file_path: row.file_path,
|
||||
file_level: row.file_level,
|
||||
module_path: row.module_path,
|
||||
updated_at: row.updated_at,
|
||||
update_source: row.update_source,
|
||||
git_commit_hash: row.git_commit_hash,
|
||||
files_changed_before_update: row.files_changed_before_update,
|
||||
metadata: row.metadata
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all CLAUDE.md update records for freshness calculation
|
||||
*/
|
||||
getAllClaudeUpdateRecords(): ClaudeUpdateRecord[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM claude_update_history
|
||||
WHERE id IN (
|
||||
SELECT MAX(id) FROM claude_update_history
|
||||
GROUP BY file_path
|
||||
)
|
||||
ORDER BY updated_at DESC
|
||||
`);
|
||||
|
||||
const rows = stmt.all() as any[];
|
||||
return rows.map(row => ({
|
||||
id: row.id,
|
||||
file_path: row.file_path,
|
||||
file_level: row.file_level,
|
||||
module_path: row.module_path,
|
||||
updated_at: row.updated_at,
|
||||
update_source: row.update_source,
|
||||
git_commit_hash: row.git_commit_hash,
|
||||
files_changed_before_update: row.files_changed_before_update,
|
||||
metadata: row.metadata
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete update records for a file
|
||||
*/
|
||||
deleteClaudeUpdateRecords(filePath: string): number {
|
||||
const stmt = this.db.prepare(`
|
||||
DELETE FROM claude_update_history
|
||||
WHERE file_path = ?
|
||||
`);
|
||||
const result = stmt.run(filePath);
|
||||
return result.changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*/
|
||||
|
||||
@@ -651,6 +651,14 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
// Write updated content
|
||||
writeFileSync(filePath, finalContent, 'utf8');
|
||||
|
||||
// Mark file as updated for freshness tracking
|
||||
try {
|
||||
const { markFileAsUpdated } = await import('../claude-freshness.js');
|
||||
markFileAsUpdated(filePath, level, 'cli_sync', initialPath, { tool, mode });
|
||||
} catch (e) {
|
||||
console.error('Failed to mark file as updated:', e);
|
||||
}
|
||||
|
||||
// Broadcast WebSocket event
|
||||
broadcastToClients({
|
||||
type: 'CLAUDE_FILE_SYNCED',
|
||||
@@ -1026,5 +1034,150 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get freshness scores for all CLAUDE.md files
|
||||
if (pathname === '/api/memory/claude/freshness' && req.method === 'GET') {
|
||||
try {
|
||||
const { calculateAllFreshness } = await import('../claude-freshness.js');
|
||||
|
||||
const projectPathParam = url.searchParams.get('path') || initialPath;
|
||||
const threshold = parseInt(url.searchParams.get('threshold') || '20', 10);
|
||||
|
||||
// Get all CLAUDE.md files
|
||||
const filesData = scanAllClaudeFiles(projectPathParam);
|
||||
|
||||
// Prepare file list for freshness calculation
|
||||
const claudeFiles: Array<{
|
||||
path: string;
|
||||
level: 'user' | 'project' | 'module';
|
||||
lastModified: string;
|
||||
}> = [];
|
||||
|
||||
if (filesData.user.main) {
|
||||
claudeFiles.push({
|
||||
path: filesData.user.main.path,
|
||||
level: 'user',
|
||||
lastModified: filesData.user.main.lastModified
|
||||
});
|
||||
}
|
||||
|
||||
if (filesData.project.main) {
|
||||
claudeFiles.push({
|
||||
path: filesData.project.main.path,
|
||||
level: 'project',
|
||||
lastModified: filesData.project.main.lastModified
|
||||
});
|
||||
}
|
||||
|
||||
for (const module of filesData.modules) {
|
||||
claudeFiles.push({
|
||||
path: module.path,
|
||||
level: 'module',
|
||||
lastModified: module.lastModified
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate freshness
|
||||
const freshnessData = calculateAllFreshness(claudeFiles, projectPathParam, threshold);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(freshnessData));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error calculating freshness:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// API: Mark a CLAUDE.md file as updated
|
||||
if (pathname === '/api/memory/claude/mark-updated' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const { path: filePath, source, metadata } = body;
|
||||
|
||||
if (!filePath) {
|
||||
return { error: 'Missing path parameter', status: 400 };
|
||||
}
|
||||
|
||||
if (!source || !['manual', 'cli_sync', 'dashboard', 'api'].includes(source)) {
|
||||
return { error: 'Invalid or missing source parameter', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const { markFileAsUpdated } = await import('../claude-freshness.js');
|
||||
|
||||
// Determine file level
|
||||
let level: 'user' | 'project' | 'module' = 'module';
|
||||
if (filePath.includes(join(homedir(), '.claude'))) {
|
||||
level = 'user';
|
||||
} else if (filePath.includes('.claude')) {
|
||||
level = 'project';
|
||||
}
|
||||
|
||||
const record = markFileAsUpdated(filePath, level, source, initialPath, metadata);
|
||||
|
||||
// Broadcast update
|
||||
broadcastToClients({
|
||||
type: 'CLAUDE_FRESHNESS_UPDATED',
|
||||
data: {
|
||||
path: filePath,
|
||||
level,
|
||||
updatedAt: record.updated_at,
|
||||
source
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
record: {
|
||||
id: record.id,
|
||||
updated_at: record.updated_at,
|
||||
filesChangedBeforeUpdate: record.files_changed_before_update
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error marking file as updated:', error);
|
||||
return { error: (error as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get update history for a CLAUDE.md file
|
||||
if (pathname === '/api/memory/claude/history' && req.method === 'GET') {
|
||||
const filePath = url.searchParams.get('path');
|
||||
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
||||
|
||||
if (!filePath) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Missing path parameter' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const { getUpdateHistory } = await import('../claude-freshness.js');
|
||||
|
||||
const records = getUpdateHistory(filePath, initialPath, limit);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
records: records.map(r => ({
|
||||
id: r.id,
|
||||
updated_at: r.updated_at,
|
||||
update_source: r.update_source,
|
||||
git_commit_hash: r.git_commit_hash,
|
||||
files_changed_before_update: r.files_changed_before_update,
|
||||
metadata: r.metadata ? JSON.parse(r.metadata) : undefined
|
||||
}))
|
||||
}));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error getting update history:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getAllToolSchemas, executeTool, executeToolWithProgress } from '../tool
|
||||
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
||||
|
||||
const SERVER_NAME = 'ccw-tools';
|
||||
const SERVER_VERSION = '6.1.4';
|
||||
const SERVER_VERSION = '6.2.0';
|
||||
|
||||
// Default enabled tools (core set)
|
||||
const DEFAULT_TOOLS: string[] = ['write_file', 'edit_file', 'read_file', 'smart_search', 'core_memory'];
|
||||
|
||||
@@ -734,6 +734,118 @@
|
||||
border-color: hsl(0, 72%, 45%);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* Freshness Tracking Styles
|
||||
* ======================================== */
|
||||
|
||||
/* Freshness badges in file tree */
|
||||
.freshness-badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
margin-left: auto;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.freshness-badge.good {
|
||||
background: hsl(142, 71%, 45%, 0.15);
|
||||
color: hsl(142, 71%, 45%);
|
||||
}
|
||||
|
||||
.freshness-badge.warn {
|
||||
background: hsl(38, 92%, 50%, 0.15);
|
||||
color: hsl(38, 92%, 50%);
|
||||
}
|
||||
|
||||
.freshness-badge.stale {
|
||||
background: hsl(0, 72%, 51%, 0.15);
|
||||
color: hsl(0, 72%, 51%);
|
||||
}
|
||||
|
||||
/* File tree item freshness states */
|
||||
.file-tree-item.freshness-stale {
|
||||
border-left: 2px solid hsl(0, 72%, 51%);
|
||||
}
|
||||
|
||||
.file-tree-item.freshness-warn {
|
||||
border-left: 2px solid hsl(38, 92%, 50%);
|
||||
}
|
||||
|
||||
.file-tree-item.freshness-good {
|
||||
border-left: 2px solid hsl(142, 71%, 45%);
|
||||
}
|
||||
|
||||
/* Freshness section in metadata panel */
|
||||
.freshness-section {
|
||||
padding: 1rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--card));
|
||||
}
|
||||
|
||||
.freshness-section h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Freshness gauge/progress bar */
|
||||
.freshness-gauge {
|
||||
position: relative;
|
||||
height: 8px;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.freshness-bar {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.freshness-bar.good {
|
||||
background: linear-gradient(90deg, hsl(142, 71%, 45%), hsl(142, 71%, 55%));
|
||||
}
|
||||
|
||||
.freshness-bar.warn {
|
||||
background: linear-gradient(90deg, hsl(38, 92%, 50%), hsl(45, 92%, 50%));
|
||||
}
|
||||
|
||||
.freshness-bar.stale {
|
||||
background: linear-gradient(90deg, hsl(0, 72%, 51%), hsl(15, 72%, 51%));
|
||||
}
|
||||
|
||||
/* Freshness value display */
|
||||
.freshness-value-display {
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--foreground));
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Update reminder warning */
|
||||
.update-reminder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: hsl(38, 92%, 50%, 0.1);
|
||||
border: 1px solid hsl(38, 92%, 50%, 0.3);
|
||||
border-radius: 0.375rem;
|
||||
color: hsl(38, 92%, 40%);
|
||||
font-size: 0.8rem;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.update-reminder i {
|
||||
flex-shrink: 0;
|
||||
color: hsl(38, 92%, 50%);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* Responsive Design
|
||||
* ======================================== */
|
||||
|
||||
@@ -1087,6 +1087,15 @@ const i18n = {
|
||||
'claudeManager.unsavedChanges': 'You have unsaved changes. Discard them?',
|
||||
'claudeManager.saved': 'File saved successfully',
|
||||
'claudeManager.saveError': 'Failed to save file',
|
||||
'claudeManager.freshness': 'Freshness',
|
||||
'claudeManager.lastContentUpdate': 'Last Content Update',
|
||||
'claudeManager.changedFiles': 'Changed Files',
|
||||
'claudeManager.filesSinceUpdate': 'files since update',
|
||||
'claudeManager.updateReminder': 'This file may need updating',
|
||||
'claudeManager.markAsUpdated': 'Mark as Updated',
|
||||
'claudeManager.markedAsUpdated': 'Marked as updated successfully',
|
||||
'claudeManager.markUpdateError': 'Failed to mark as updated',
|
||||
'claudeManager.never': 'Never tracked',
|
||||
|
||||
// Graph Explorer
|
||||
'nav.graphExplorer': 'Graph',
|
||||
@@ -2377,6 +2386,15 @@ const i18n = {
|
||||
'claudeManager.unsavedChanges': '您有未保存的更改。是否放弃?',
|
||||
'claudeManager.saved': '文件保存成功',
|
||||
'claudeManager.saveError': '文件保存失败',
|
||||
'claudeManager.freshness': '新鲜度',
|
||||
'claudeManager.lastContentUpdate': '上次内容更新',
|
||||
'claudeManager.changedFiles': '变动文件',
|
||||
'claudeManager.filesSinceUpdate': '个文件自上次更新后变动',
|
||||
'claudeManager.updateReminder': '此文件可能需要更新',
|
||||
'claudeManager.markAsUpdated': '标记为已更新',
|
||||
'claudeManager.markedAsUpdated': '已成功标记为已更新',
|
||||
'claudeManager.markUpdateError': '标记更新失败',
|
||||
'claudeManager.never': '从未追踪',
|
||||
|
||||
// Graph Explorer
|
||||
'nav.graphExplorer': '图谱',
|
||||
|
||||
@@ -17,6 +17,8 @@ var fileTreeExpanded = {
|
||||
modules: {}
|
||||
};
|
||||
var searchQuery = '';
|
||||
var freshnessData = {}; // { [filePath]: FreshnessResult }
|
||||
var freshnessSummary = null;
|
||||
|
||||
// ========== Main Render Function ==========
|
||||
async function renderClaudeManager() {
|
||||
@@ -37,6 +39,7 @@ async function renderClaudeManager() {
|
||||
|
||||
// Load data
|
||||
await loadClaudeFiles();
|
||||
await loadFreshnessData();
|
||||
|
||||
// Render layout
|
||||
container.innerHTML = '<div class="claude-manager-view">' +
|
||||
@@ -85,10 +88,60 @@ async function loadClaudeFiles() {
|
||||
|
||||
async function refreshClaudeFiles() {
|
||||
await loadClaudeFiles();
|
||||
await loadFreshnessData();
|
||||
await renderClaudeManager();
|
||||
addGlobalNotification('success', t('claudeManager.refreshed'), null, 'CLAUDE.md');
|
||||
}
|
||||
|
||||
// ========== Freshness Data Loading ==========
|
||||
async function loadFreshnessData() {
|
||||
try {
|
||||
var res = await fetch('/api/memory/claude/freshness?path=' + encodeURIComponent(projectPath || ''));
|
||||
if (!res.ok) throw new Error('Failed to load freshness data');
|
||||
var data = await res.json();
|
||||
|
||||
// Build lookup map
|
||||
freshnessData = {};
|
||||
if (data.files) {
|
||||
data.files.forEach(function(f) {
|
||||
freshnessData[f.path] = f;
|
||||
});
|
||||
}
|
||||
freshnessSummary = data.summary || null;
|
||||
} catch (error) {
|
||||
console.error('Error loading freshness data:', error);
|
||||
freshnessData = {};
|
||||
freshnessSummary = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function markFileAsUpdated() {
|
||||
if (!selectedFile) return;
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/memory/claude/mark-updated', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
path: selectedFile.path,
|
||||
source: 'dashboard'
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to mark file as updated');
|
||||
|
||||
addGlobalNotification('success', t('claudeManager.markedAsUpdated') || 'Marked as updated', null, 'CLAUDE.md');
|
||||
|
||||
// Reload freshness data
|
||||
await loadFreshnessData();
|
||||
renderFileTree();
|
||||
renderFileMetadata();
|
||||
} catch (error) {
|
||||
console.error('Error marking file as updated:', error);
|
||||
addGlobalNotification('error', t('claudeManager.markUpdateError') || 'Failed to mark as updated', null, 'CLAUDE.md');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== File Tree Rendering ==========
|
||||
function renderFileTree() {
|
||||
var container = document.getElementById('claude-file-tree');
|
||||
@@ -183,11 +236,30 @@ function renderFileTreeItem(file, indentLevel) {
|
||||
var indentPx = indentLevel * 1.5;
|
||||
var safeId = file.id.replace(/'/g, "'");
|
||||
|
||||
return '<div class="file-tree-item' + (isSelected ? ' selected' : '') + '" ' +
|
||||
// Get freshness data for this file
|
||||
var fd = freshnessData[file.path];
|
||||
var freshnessClass = '';
|
||||
var freshnessBadge = '';
|
||||
|
||||
if (fd) {
|
||||
if (fd.freshness >= 75) {
|
||||
freshnessClass = ' freshness-good';
|
||||
freshnessBadge = '<span class="freshness-badge good">' + fd.freshness + '%</span>';
|
||||
} else if (fd.freshness >= 50) {
|
||||
freshnessClass = ' freshness-warn';
|
||||
freshnessBadge = '<span class="freshness-badge warn">' + fd.freshness + '%</span>';
|
||||
} else {
|
||||
freshnessClass = ' freshness-stale';
|
||||
freshnessBadge = '<span class="freshness-badge stale">' + fd.freshness + '%</span>';
|
||||
}
|
||||
}
|
||||
|
||||
return '<div class="file-tree-item' + freshnessClass + (isSelected ? ' selected' : '') + '" ' +
|
||||
'onclick="selectClaudeFile(\'' + safeId + '\')" ' +
|
||||
'style="padding-left: ' + indentPx + 'rem;">' +
|
||||
'<i data-lucide="file-text" class="w-4 h-4"></i>' +
|
||||
'<span class="file-name">' + escapeHtml(file.name) + '</span>' +
|
||||
freshnessBadge +
|
||||
(file.parentDirectory ? '<span class="file-path-hint">' + escapeHtml(file.parentDirectory) + '</span>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
@@ -446,6 +518,38 @@ function renderFileMetadata() {
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Freshness section
|
||||
var fd = freshnessData[selectedFile.path];
|
||||
if (fd) {
|
||||
var freshnessBarClass = fd.freshness >= 75 ? 'good' : fd.freshness >= 50 ? 'warn' : 'stale';
|
||||
html += '<div class="metadata-section freshness-section">' +
|
||||
'<h4><i data-lucide="activity" class="w-4 h-4"></i> ' + (t('claudeManager.freshness') || 'Freshness') + '</h4>' +
|
||||
'<div class="freshness-gauge">' +
|
||||
'<div class="freshness-bar ' + freshnessBarClass + '" style="width: ' + fd.freshness + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="freshness-value-display">' + fd.freshness + '%</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + (t('claudeManager.lastContentUpdate') || 'Last Content Update') + '</span>' +
|
||||
'<span class="value">' + (fd.lastUpdated ? formatDate(fd.lastUpdated) : (t('claudeManager.never') || 'Never tracked')) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + (t('claudeManager.changedFiles') || 'Changed Files') + '</span>' +
|
||||
'<span class="value">' + fd.changedFilesCount + ' ' + (t('claudeManager.filesSinceUpdate') || 'files since update') + '</span>' +
|
||||
'</div>';
|
||||
|
||||
if (fd.needsUpdate) {
|
||||
html += '<div class="update-reminder">' +
|
||||
'<i data-lucide="alert-triangle" class="w-4 h-4"></i>' +
|
||||
'<span>' + (t('claudeManager.updateReminder') || 'This file may need updating') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '<button class="btn btn-sm btn-secondary full-width" onclick="markFileAsUpdated()">' +
|
||||
'<i data-lucide="check-circle" class="w-4 h-4"></i> ' + (t('claudeManager.markAsUpdated') || 'Mark as Updated') +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '<div class="metadata-section">' +
|
||||
'<h4>' + t('claudeManager.actions') + '</h4>';
|
||||
|
||||
@@ -536,10 +640,12 @@ async function syncFileWithCLI() {
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Reload file content
|
||||
// Reload file content and freshness data
|
||||
var fileData = await loadFileContent(selectedFile.path);
|
||||
if (fileData) {
|
||||
selectedFile = fileData;
|
||||
await loadFreshnessData();
|
||||
renderFileTree();
|
||||
renderFileViewer();
|
||||
renderFileMetadata();
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ const ParamsSchema = z.object({
|
||||
format: z.enum(['json', 'text', 'pretty']).default('json'),
|
||||
languages: z.array(z.string()).optional(),
|
||||
limit: z.number().default(20),
|
||||
enrich: z.boolean().default(false),
|
||||
// Additional fields for internal functions
|
||||
file: z.string().optional(),
|
||||
key: z.string().optional(),
|
||||
@@ -516,7 +517,7 @@ async function initIndex(params: Params): Promise<ExecuteResult> {
|
||||
* @returns Execution result
|
||||
*/
|
||||
async function searchCode(params: Params): Promise<ExecuteResult> {
|
||||
const { query, path = '.', limit = 20, mode = 'auto' } = params;
|
||||
const { query, path = '.', limit = 20, mode = 'auto', enrich = false } = params;
|
||||
|
||||
if (!query) {
|
||||
return { success: false, error: 'Query is required for search action' };
|
||||
@@ -537,6 +538,10 @@ async function searchCode(params: Params): Promise<ExecuteResult> {
|
||||
const cliMode = modeMap[mode] || 'auto';
|
||||
const args = ['search', query, '--limit', limit.toString(), '--mode', cliMode, '--json'];
|
||||
|
||||
if (enrich) {
|
||||
args.push('--enrich');
|
||||
}
|
||||
|
||||
const result = await executeCodexLens(args, { cwd: path });
|
||||
|
||||
if (result.success && result.output) {
|
||||
@@ -557,7 +562,7 @@ async function searchCode(params: Params): Promise<ExecuteResult> {
|
||||
* @returns Execution result
|
||||
*/
|
||||
async function searchFiles(params: Params): Promise<ExecuteResult> {
|
||||
const { query, path = '.', limit = 20, mode = 'auto' } = params;
|
||||
const { query, path = '.', limit = 20, mode = 'auto', enrich = false } = params;
|
||||
|
||||
if (!query) {
|
||||
return { success: false, error: 'Query is required for search_files action' };
|
||||
@@ -578,6 +583,10 @@ async function searchFiles(params: Params): Promise<ExecuteResult> {
|
||||
const cliMode = modeMap[mode] || 'auto';
|
||||
const args = ['search', query, '--files-only', '--limit', limit.toString(), '--mode', cliMode, '--json'];
|
||||
|
||||
if (enrich) {
|
||||
args.push('--enrich');
|
||||
}
|
||||
|
||||
const result = await executeCodexLens(args, { cwd: path });
|
||||
|
||||
if (result.success && result.output) {
|
||||
@@ -764,6 +773,9 @@ Usage:
|
||||
codex_lens(action="search", query="func", mode="hybrid") # Force hybrid search
|
||||
codex_lens(action="search_files", query="x") # Search, return paths only
|
||||
|
||||
Graph Enrichment:
|
||||
codex_lens(action="search", query="func", enrich=true) # Enrich results with code relationships
|
||||
|
||||
Search Modes:
|
||||
- auto: Auto-detect (hybrid if embeddings exist, exact otherwise) [default]
|
||||
- exact/text: Exact FTS for code identifiers
|
||||
@@ -820,6 +832,11 @@ Note: For advanced operations (config, status, clean), use CLI directly: codexle
|
||||
description: 'Maximum number of search results (for search and search_files actions)',
|
||||
default: 20,
|
||||
},
|
||||
enrich: {
|
||||
type: 'boolean',
|
||||
description: 'Enrich search results with code graph relationships (calls, imports)',
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
required: ['action'],
|
||||
},
|
||||
|
||||
@@ -36,6 +36,7 @@ const ParamsSchema = z.object({
|
||||
includeHidden: z.boolean().default(false),
|
||||
languages: z.array(z.string()).optional(),
|
||||
limit: z.number().default(10),
|
||||
enrich: z.boolean().default(false),
|
||||
});
|
||||
|
||||
type Params = z.infer<typeof ParamsSchema>;
|
||||
@@ -59,11 +60,21 @@ interface ExactMatch {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface RelationshipInfo {
|
||||
type: string; // 'calls', 'imports', 'called_by', 'imported_by'
|
||||
direction: 'outgoing' | 'incoming';
|
||||
target?: string; // Target symbol name (for outgoing)
|
||||
source?: string; // Source symbol name (for incoming)
|
||||
file: string; // File path
|
||||
line?: number; // Line number
|
||||
}
|
||||
|
||||
interface SemanticMatch {
|
||||
file: string;
|
||||
score: number;
|
||||
content: string;
|
||||
symbol: string | null;
|
||||
relationships?: RelationshipInfo[];
|
||||
}
|
||||
|
||||
interface GraphMatch {
|
||||
@@ -635,7 +646,7 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
|
||||
* Requires index
|
||||
*/
|
||||
async function executeCodexLensExactMode(params: Params): Promise<SearchResult> {
|
||||
const { query, path = '.', maxResults = 10 } = params;
|
||||
const { query, path = '.', maxResults = 10, enrich = false } = params;
|
||||
|
||||
if (!query) {
|
||||
return {
|
||||
@@ -657,6 +668,9 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
|
||||
const indexStatus = await checkIndexStatus(path);
|
||||
|
||||
const args = ['search', query, '--limit', maxResults.toString(), '--mode', 'exact', '--json'];
|
||||
if (enrich) {
|
||||
args.push('--enrich');
|
||||
}
|
||||
const result = await executeCodexLens(args, { cwd: path });
|
||||
|
||||
if (!result.success) {
|
||||
@@ -707,7 +721,7 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
|
||||
* Requires index with embeddings
|
||||
*/
|
||||
async function executeHybridMode(params: Params): Promise<SearchResult> {
|
||||
const { query, path = '.', maxResults = 10 } = params;
|
||||
const { query, path = '.', maxResults = 10, enrich = false } = params;
|
||||
|
||||
if (!query) {
|
||||
return {
|
||||
@@ -729,6 +743,9 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
|
||||
const indexStatus = await checkIndexStatus(path);
|
||||
|
||||
const args = ['search', query, '--limit', maxResults.toString(), '--mode', 'hybrid', '--json'];
|
||||
if (enrich) {
|
||||
args.push('--enrich');
|
||||
}
|
||||
const result = await executeCodexLens(args, { cwd: path });
|
||||
|
||||
if (!result.success) {
|
||||
@@ -958,6 +975,9 @@ export const schema: ToolSchema = {
|
||||
smart_search(action="init") # Create FTS index for current directory
|
||||
smart_search(action="status") # Check index and embedding status
|
||||
|
||||
**Graph Enrichment:**
|
||||
smart_search(query="func", enrich=true) # Enrich results with code relationships (calls, imports, called_by, imported_by)
|
||||
|
||||
**Modes:** auto (intelligent routing), hybrid (semantic, needs index), exact (FTS), ripgrep (fast, no index), priority (fallback: hybrid→exact→ripgrep)`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
@@ -1021,6 +1041,11 @@ export const schema: ToolSchema = {
|
||||
items: { type: 'string' },
|
||||
description: 'Languages to index (for init action). Example: ["javascript", "typescript"]',
|
||||
},
|
||||
enrich: {
|
||||
type: 'boolean',
|
||||
description: 'Enrich search results with code graph relationships (calls, imports, called_by, imported_by).',
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user