Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/cli/commands/queryFilesCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Command } from 'commander';
import { executeHandler } from '../types.js';

export const queryFilesCommand = new Command('query-files')
.description('Query refs table by file name match (substring/prefix/wildcard/regex/fuzzy)')
.argument('<pattern>', 'File name pattern to search')
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 NIT: 术语不清晰

命令描述中的 'refs table' 可能不易理解,建议使用更直观的描述

建议: 修改为 'Query files by name pattern (substring/prefix/wildcard/regex/fuzzy)'

Suggested change
.argument('<pattern>', 'File name pattern to search')
.description('Query files by name pattern (substring/prefix/wildcard/regex/fuzzy)')

.option('-p, --path <path>', 'Path inside the repository', '.')
.option('--limit <n>', 'Limit results', '50')
.option('--mode <mode>', 'Mode: substring|prefix|wildcard|regex|fuzzy (default: auto)')
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 SUGGESTION: mode 选项缺少默认值说明

选项描述中提到默认值是 auto,但命令签名中未明确显示,可能导致用户困惑

建议: 修改为:.option('--mode ', 'Mode (default: substring|prefix|wildcard|regex|fuzzy or auto)', 'auto')

Suggested change
.option('--mode <mode>', 'Mode: substring|prefix|wildcard|regex|fuzzy (default: auto)')
.option('--mode <mode>', 'Mode (default: auto)', 'auto')

.option('--case-insensitive', 'Case-insensitive matching', false)
.option('--max-candidates <n>', 'Max candidates to fetch before filtering', '1000')
.option('--lang <lang>', 'Language: auto|all|java|ts|python|go|rust|c|markdown|yaml', 'auto')
.option('--with-repo-map', 'Attach a lightweight repo map (ranked files + top symbols + wiki links)', false)
.option('--repo-map-files <n>', 'Max repo map files', '20')
.option('--repo-map-symbols <n>', 'Max repo map symbols per file', '5')
.option('--wiki <dir>', 'Wiki directory (default: docs/wiki or wiki)', '')
.action(async (pattern, options) => {
await executeHandler('query-files', { pattern, ...options });
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: 选项值类型未转换

commander.js 将所有选项值作为字符串返回,但 limit、max-candidates、repo-map-files、repo-map-symbols 等应该是 number 类型,直接传递可能导致 executeHandler 中类型错误或比较失败

建议: 在传递前转换数值类型:
const limit = parseInt(options.limit, 10);
const maxCandidates = parseInt(options.maxCandidates, 10);
await executeHandler('query-files', { pattern, limit, maxCandidates, ...options });

Suggested change
await executeHandler('query-files', { pattern, ...options });
const limit = parseInt(options.limit, 10);
const maxCandidates = parseInt(options.maxCandidates, 10);
await executeHandler('query-files', { pattern, limit, maxCandidates, ...options });

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: 缺少 pattern 参数验证

未验证 pattern 是否为空字符串,空 pattern 可能导致全表扫描或意外行为

建议: 添加验证:
if (!pattern || pattern.trim() === '') {
console.error('Error: Pattern cannot be empty');
process.exit(1);
}

Suggested change
await executeHandler('query-files', { pattern, ...options });
if (!pattern || pattern.trim() === '') {
console.error('Error: Pattern cannot be empty');
process.exit(1);
}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: 数值选项缺少范围验证

limit、max-candidates 等数值选项未验证是否为有效的正整数,负数或非数字可能导致错误

建议: 添加验证函数:
const safeParseInt = (val: string, defaultVal: number, min: number) => {
const num = parseInt(val, 10);
return isNaN(num) || num < min ? defaultVal : num;
};

Suggested change
await executeHandler('query-files', { pattern, ...options });
const limit = safeParseInt(options.limit, 50, 1);
const maxCandidates = safeParseInt(options.maxCandidates, 1000, 1);

});
303 changes: 303 additions & 0 deletions src/cli/handlers/queryFilesHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
import path from 'path';
import fs from 'fs-extra';
import { inferWorkspaceRoot, resolveGitRoot } from '../../core/git';
import { defaultDbDir, openTablesByLang, type IndexLang } from '../../core/lancedb';
import { queryManifestWorkspace } from '../../core/workspace';
import { inferSymbolSearchMode, type SymbolSearchMode } from '../../core/symbolSearch';
import { createLogger } from '../../core/log';
import { resolveLangs } from '../../core/indexCheck';
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import resolveLangs.

Suggested change
import { resolveLangs } from '../../core/indexCheck';

Copilot uses AI. Check for mistakes.
import { generateRepoMap, type FileRank } from '../../core/repoMap';
import type { CLIResult, CLIError } from '../types';
import { success, error } from '../types';
import { resolveRepoContext, validateIndex, resolveLanguages, type RepoContext } from '../helpers';
import type { SearchFilesInput } from '../schemas/queryFilesSchemas';

function isCLIError(value: unknown): value is CLIError {
return typeof value === 'object' && value !== null && 'ok' in value && (value as any).ok === false;
}

async function buildRepoMapAttachment(
repoRoot: string,
options: { wiki: string; repoMapFiles: number; repoMapSymbols: number }
): Promise<{ enabled: boolean; wikiDir: string; files: FileRank[] } | { enabled: boolean; skippedReason: string }> {
try {
const wikiDir = resolveWikiDir(repoRoot, options.wiki);
const files = await generateRepoMap({
repoRoot,
maxFiles: options.repoMapFiles,
maxSymbolsPerFile: options.repoMapSymbols,
wikiDir,
});
return { enabled: true, wikiDir, files };
} catch (e: any) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: Error message 可能泄露敏感信息

catch 块直接使用 String(e?.message ?? e) 将错误信息转换为字符串,可能暴露内部路径、配置或敏感系统信息

建议: 使用通用的错误消息,在日志中记录完整错误,开发环境返回详细错误

Suggested change
} catch (e: any) {
const safeMessage = process.env.NODE_ENV === 'production' ? 'Failed to build repo map' : String(e?.message ?? e);
return { enabled: false, skippedReason: safeMessage };

return { enabled: false, skippedReason: String(e?.message ?? e) };
}
}

function resolveWikiDir(repoRoot: string, wikiOpt: string): string {
const w = String(wikiOpt ?? '').trim();
if (w) return path.resolve(repoRoot, w);
const candidates = [path.join(repoRoot, 'docs', 'wiki'), path.join(repoRoot, 'wiki')];
for (const c of candidates) {
if (fs.existsSync(c)) return c;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: fs.existsSync 已废弃

在 Node.js 14+ 中推荐使用 fs.statSync 或 fs.accessSync 配合异常处理,性能更好且更符合 Node.js 风格

建议: 使用 try-catch 块包装 fs.accessSync 检查

Suggested change
if (fs.existsSync(c)) return c;
for (const c of candidates) {
try {
fs.accessSync(c);
return c;
} catch { /* continue */ }
}

}
return '';
Comment on lines +37 to +44
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--wiki is resolved with path.resolve(repoRoot, w) without checking that the resulting path stays within repoRoot. Since generateRepoMap will read markdown files from wikiDir, this allows reading arbitrary filesystem locations when a user passes --wiki ../../.... Consider rejecting paths that escape repoRoot (similar to resolveWikiDirInsideRepo in the MCP handler) and returning a clear error or disabling wiki attachment in that case.

Copilot uses AI. Check for mistakes.
}

function inferLangFromFile(file: string): IndexLang {
const f = String(file);
if (f.endsWith('.md') || f.endsWith('.mdx')) return 'markdown';
if (f.endsWith('.yml') || f.endsWith('.yaml')) return 'yaml';
if (f.endsWith('.java')) return 'java';
if (f.endsWith('.c') || f.endsWith('.h')) return 'c';
if (f.endsWith('.go')) return 'go';
if (f.endsWith('.py')) return 'python';
if (f.endsWith('.rs')) return 'rust';
return 'ts';
}

function filterWorkspaceRowsByLang(rows: any[], langSel: string): any[] {
const sel = String(langSel ?? 'auto');
if (sel === 'auto' || sel === 'all') return rows;
const target = sel as IndexLang;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: 不安全的类型断言

将字符串断言为 IndexLang 类型时未验证是否有效值,可能导致后续代码在非法语言类型上失败

建议: 先验证输入值是否在 IndexLang 的合法值列表中

Suggested change
const target = sel as IndexLang;
const validLangs: IndexLang[] = ['ts', 'python', 'java', 'rust', 'go', 'c', 'markdown', 'yaml'];
const target = validLangs.includes(sel as IndexLang) ? (sel as IndexLang) : undefined;
return rows.filter(r => !target || inferLangFromFile(String((r as any).file ?? '')) === target);

return rows.filter(r => inferLangFromFile(String((r as any).file ?? '')) === target);
}
Comment on lines +19 to +64
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file duplicates several helper utilities from queryHandlers.ts (e.g., buildRepoMapAttachment, resolveWikiDir, inferLangFromFile, filterWorkspaceRowsByLang). Since these helpers are now used in multiple commands, consider extracting them into a shared module to avoid divergent behavior/fixes across commands (e.g., wiki-dir validation, language inference changes).

Copilot uses AI. Check for mistakes.

function escapeQuotes(s: string): string {
return s.replace(/'/g, "''");
}

function buildFileWhere(pattern: string, mode: SymbolSearchMode, caseInsensitive: boolean): string | null {
const safe = escapeQuotes(pattern);
if (!safe) return null;
const likeOp = caseInsensitive ? 'ILIKE' : 'LIKE';

if (mode === 'prefix') {
return `file ${likeOp} '${safe}%'`;
}

if (mode === 'substring' || mode === 'wildcard') {
return `file ${likeOp} '%${safe}%'`;
}

Comment on lines +70 to +82
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wildcard mode is prefiltered using LIKE '%${pattern}%' with the raw glob pattern (including */?). For patterns like src/*/handlers this WHERE clause will never match, yielding zero candidates and therefore zero results even though the in-memory glob filter would match. Translate glob to a SQL LIKE pattern (*%, ?_, escape literal %/_) or skip the SQL WHERE for wildcard and do in-memory filtering after fetching candidates.

Suggested change
function buildFileWhere(pattern: string, mode: SymbolSearchMode, caseInsensitive: boolean): string | null {
const safe = escapeQuotes(pattern);
if (!safe) return null;
const likeOp = caseInsensitive ? 'ILIKE' : 'LIKE';
if (mode === 'prefix') {
return `file ${likeOp} '${safe}%'`;
}
if (mode === 'substring' || mode === 'wildcard') {
return `file ${likeOp} '%${safe}%'`;
}
/**
* Convert a glob pattern to a SQL LIKE pattern.
*
* - Escapes SQL LIKE wildcards (% and _) using backslash.
* - Escapes backslash itself.
* - Converts glob '*' to '%' and '?' to '_'.
*/
function globToSqlLike(pattern: string): string {
// First escape backslash, then SQL LIKE wildcards.
let like = pattern.replace(/\\/g, '\\\\').replace(/([%_])/g, '\\$1');
// Then convert glob wildcards to SQL LIKE wildcards.
like = like.replace(/\*/g, '%').replace(/\?/g, '_');
return like;
}
function buildFileWhere(pattern: string, mode: SymbolSearchMode, caseInsensitive: boolean): string | null {
const likeOp = caseInsensitive ? 'ILIKE' : 'LIKE';
if (!pattern) return null;
if (mode === 'prefix') {
const safe = escapeQuotes(pattern);
if (!safe) return null;
return `file ${likeOp} '${safe}%'`;
}
if (mode === 'substring') {
const safe = escapeQuotes(pattern);
if (!safe) return null;
return `file ${likeOp} '%${safe}%'`;
}
if (mode === 'wildcard') {
const likePattern = globToSqlLike(pattern);
const safeLike = escapeQuotes(likePattern);
if (!safeLike) return null;
// Use ESCAPE '\' so that backslash-escaped % and _ are treated as literals.
return `file ${likeOp} '${safeLike}' ESCAPE '\\\\'`;
}

Copilot uses AI. Check for mistakes.
// For regex and fuzzy, we'll handle them in memory after fetching
return null;
}

function buildRegex(pattern: string, caseInsensitive: boolean): RegExp | null {
try {
const flags = caseInsensitive ? 'i' : '';
return new RegExp(pattern, flags);
} catch {
return null;
}
}

function globToRegex(pattern: string, caseInsensitive: boolean): RegExp | null {
try {
const body = pattern
.split('')
.map(ch => {
if (ch === '*') return '.*';
if (ch === '?') return '.';
return escapeRegex(ch);
})
.join('');
const flags = caseInsensitive ? 'i' : '';
return new RegExp(`^${body}$`, flags);
} catch {
return null;
}
}

function escapeRegex(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function filterAndRankFileRows<T extends Record<string, any>>(
rows: T[],
pattern: string,
mode: SymbolSearchMode,
caseInsensitive: boolean,
limit: number
): T[] {
const getFile = (r: any) => String(r?.file ?? '');
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: 重复的类型断言和不安全的 any 使用

getFile 函数内部和 filterAndRankFileRows 参数都使用 any 类型,绕过了 TypeScript 的类型检查,可能导致运行时错误

建议: 定义明确的接口类型,如:interface FileRow { file: string; lang?: string }

Suggested change
const getFile = (r: any) => String(r?.file ?? '');
interface FileRow { file: string; lang?: string }
const getFile = (r: FileRow) => String(r?.file ?? '');

const finalLimit = Math.max(1, limit);

if (mode === 'substring' || mode === 'prefix') {
const p = caseInsensitive ? pattern.toLowerCase() : pattern;
const filtered = rows.filter(r => {
const f = getFile(r);
const fs = caseInsensitive ? f.toLowerCase() : f;
return mode === 'prefix' ? fs.startsWith(p) : fs.includes(p);
});
return filtered.slice(0, finalLimit);
}

if (mode === 'wildcard') {
const re = globToRegex(pattern, caseInsensitive);
if (!re) return [];
const filtered = rows.filter(r => re!.test(getFile(r)));
return filtered.slice(0, finalLimit);
}

if (mode === 'regex') {
const re = buildRegex(pattern, caseInsensitive);
if (!re) return [];
const filtered = rows.filter(r => re!.test(getFile(r)));
return filtered.slice(0, finalLimit);
}

// Fuzzy matching for files
const scored = rows
.map(r => {
const f = getFile(r);
const score = fuzzyFileScore(pattern, f, caseInsensitive);
return { r, score };
})
.filter(x => x.score >= 0)
.sort((a, b) => b.score - a.score)
.slice(0, finalLimit);

return scored.map(x => x.r);
}

function fuzzyFileScore(needle: string, haystack: string, caseInsensitive: boolean): number {
if (!needle) return 0;
const n = caseInsensitive ? needle.toLowerCase() : needle;
const h = caseInsensitive ? haystack.toLowerCase() : haystack;

let i = 0;
let score = 0;
let lastMatch = -2;

for (let j = 0; j < h.length && i < n.length; j++) {
if (h[j] === n[i]) {
score += j === lastMatch + 1 ? 2 : 1;
lastMatch = j;
i++;
}
}

if (i < n.length) return -1;
return score;
}

export async function handleSearchFiles(input: SearchFilesInput): Promise<CLIResult | CLIError> {
const log = createLogger({ component: 'cli', cmd: 'query-files' });
const startedAt = Date.now();

const repoRoot = await resolveGitRoot(path.resolve(input.path));
const mode = inferSymbolSearchMode(input.pattern, input.mode);

if (inferWorkspaceRoot(repoRoot)) {
const res = await queryManifestWorkspace({
manifestRepoRoot: repoRoot,
keyword: input.pattern,
limit: input.maxCandidates,
});
const filteredByLang = filterWorkspaceRowsByLang(res.rows, input.lang);
const rows = filterAndRankFileRows(
filteredByLang,
input.pattern,
mode,
input.caseInsensitive,
input.limit
);
log.info('query_files', {
ok: true,
repoRoot,
workspace: true,
mode,
case_insensitive: input.caseInsensitive,
limit: input.limit,
max_candidates: input.maxCandidates,
candidates: res.rows.length,
rows: rows.length,
duration_ms: Date.now() - startedAt,
});
const repoMap = input.withRepoMap
? { enabled: false, skippedReason: 'workspace_mode_not_supported' }
: undefined;
return success({ ...res, rows, ...(repoMap ? { repo_map: repoMap } : {}) });
Comment on lines +194 to +222
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workspace mode currently calls queryManifestWorkspace with keyword: input.pattern, but queryManifestWorkspace queries refs by symbol ILIKE ... (not by file). This makes query-files in manifests workspaces unreliable/incorrect (often returning no candidates unless the file pattern also appears in a symbol). Consider adding a workspace query that filters on the file column (or disabling workspace support for query-files with a clear error) and, for wildcard/regex/fuzzy, using an appropriate coarse token/prefix for candidate retrieval.

Suggested change
const res = await queryManifestWorkspace({
manifestRepoRoot: repoRoot,
keyword: input.pattern,
limit: input.maxCandidates,
});
const filteredByLang = filterWorkspaceRowsByLang(res.rows, input.lang);
const rows = filterAndRankFileRows(
filteredByLang,
input.pattern,
mode,
input.caseInsensitive,
input.limit
);
log.info('query_files', {
ok: true,
repoRoot,
workspace: true,
mode,
case_insensitive: input.caseInsensitive,
limit: input.limit,
max_candidates: input.maxCandidates,
candidates: res.rows.length,
rows: rows.length,
duration_ms: Date.now() - startedAt,
});
const repoMap = input.withRepoMap
? { enabled: false, skippedReason: 'workspace_mode_not_supported' }
: undefined;
return success({ ...res, rows, ...(repoMap ? { repo_map: repoMap } : {}) });
const durationMs = Date.now() - startedAt;
log.info('query_files', {
ok: false,
repoRoot,
workspace: true,
mode,
case_insensitive: input.caseInsensitive,
limit: input.limit,
max_candidates: input.maxCandidates,
candidates: 0,
rows: 0,
duration_ms: durationMs,
error: 'workspace_mode_not_supported_for_query_files',
});
return error({
message:
'query-files does not currently support workspace manifests. ' +
'Please run this command from a non-workspace repository root or disable workspace mode.',
code: 'workspace_mode_not_supported_for_query_files',
});

Copilot uses AI. Check for mistakes.
}

const ctxOrError = await resolveRepoContext(input.path);

if (isCLIError(ctxOrError)) {
return ctxOrError;
}

const ctx = ctxOrError as RepoContext;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: 不必要的类型断言

isCLIError 已通过类型守卫确认 ctxOrError 的类型,此处 as RepoContext 是多余的,应该直接赋值

建议: 移除类型断言,直接赋值

Suggested change
const ctx = ctxOrError as RepoContext;
const ctx = ctxOrError;
// 或者利用类型守卫后的类型推断


const validationError = validateIndex(ctx);
if (validationError) {
return validationError;
}

const langs = resolveLanguages(ctx.meta, input.lang);
if (langs.length === 0) {
return error('lang_not_available', {
lang: input.lang,
available: ctx.meta?.languages ?? [],
});
}

try {
const dbDir = defaultDbDir(ctx.repoRoot);
const dim = typeof ctx.meta?.dim === 'number' ? ctx.meta.dim : 256;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: dim 默认值处理不完整

ctx.meta?.dim 可能是浮点数或非正数,但代码仅检查是否为 number 类型,可能导致 LanceDB 查询失败

建议: 增加对 dim 值的验证:确保是正整数

Suggested change
const dim = typeof ctx.meta?.dim === 'number' ? ctx.meta.dim : 256;
const dim = typeof ctx.meta?.dim === 'number' && ctx.meta.dim > 0 ? Math.floor(ctx.meta.dim) : 256;

const { byLang } = await openTablesByLang({ dbDir, dim, mode: 'open_only', languages: langs as IndexLang[] });

// Build WHERE clause based on mode
const where = buildFileWhere(input.pattern, mode, input.caseInsensitive);

const candidates: any[] = [];
for (const lang of langs) {
const t = byLang[lang as IndexLang];
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 SUGGESTION: 循环中重复的类型检查

每次循环迭代都进行类型断言,可考虑提前验证 langs 数组中的值

建议: 在循环前过滤掉无效的语言类型

Suggested change
const t = byLang[lang as IndexLang];
const validLangs = langs.filter((l): l is IndexLang =>
['ts', 'python', 'java', 'rust', 'go', 'c', 'markdown', 'yaml'].includes(l)
);

if (!t) continue;

// Fetch candidates based on mode
// For regex/fuzzy, we fetch all and filter in memory
const shouldFetchAll = mode === 'regex' || mode === 'fuzzy';
const rows = shouldFetchAll
? await t.refs.query().limit(input.maxCandidates).toArray()
: where
? await t.refs.query().where(where).limit(input.maxCandidates).toArray()
: await t.refs.query().limit(input.maxCandidates).toArray();

for (const r of rows as any[]) candidates.push({ ...r, lang });
}

// Filter and rank by file name
const rows = filterAndRankFileRows(candidates, input.pattern, mode, input.caseInsensitive, input.limit);

log.info('query_files', {
ok: true,
repoRoot: ctx.repoRoot,
workspace: false,
lang: input.lang,
langs,
mode,
case_insensitive: input.caseInsensitive,
limit: input.limit,
max_candidates: input.maxCandidates,
candidates: candidates.length,
rows: rows.length,
duration_ms: Date.now() - startedAt,
});

const repoMap = input.withRepoMap ? await buildRepoMapAttachment(ctx.repoRoot, input) : undefined;

return success({
repoRoot: ctx.repoRoot,
count: rows.length,
lang: input.lang,
rows,
...(repoMap ? { repo_map: repoMap } : {}),
});
} catch (e) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 NIT: catch 块类型定义不完整

catch 子句参数 e 隐式为 unknown 类型,代码使用 instanceof Error 判断是正确的,但最终仍使用 String() 转换可能丢失错误堆栈

建议: 保留完整的错误对象用于日志记录

Suggested change
} catch (e) {
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Unknown error';
log.error('query_files', { ok: false, duration_ms: Date.now() - startedAt, err: message, stack: e instanceof Error ? e.stack : undefined });
return error('query_files_failed', { message });
}

const message = e instanceof Error ? e.message : String(e);
log.error('query_files', { ok: false, duration_ms: Date.now() - startedAt, err: message });
return error('query_files_failed', { message });
}
}
6 changes: 6 additions & 0 deletions src/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import {
import { SemanticSearchSchema } from './schemas/semanticSchemas';
import { IndexRepoSchema } from './schemas/indexSchemas';
import { SearchSymbolsSchema } from './schemas/querySchemas';
import { SearchFilesSchema } from './schemas/queryFilesSchemas';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 NIT: 导入语句位置不符合现有排序规则

现有的导入语句按字母顺序排列(archive, index, query, semantic, status),新添加的 SearchFilesSchema 插在了 IndexRepoSchema 和 SearchSymbolsSchema 之间,破坏了排序一致性

建议: 将 SearchFilesSchema 导入移到第26行之后,使其位于 querySchemas 导入之后

Suggested change
import { SearchFilesSchema } from './schemas/queryFilesSchemas';
import { SearchSymbolsSchema } from './schemas/querySchemas';
import { SearchFilesSchema } from './schemas/queryFilesSchemas';

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 NIT: Import grouping consistency

新增的 import 语句应该保持与现有代码一致的分组顺序。当前 schema 和 handler 的导入混在一起,建议将所有 schema 导入分组、handler 导入分组

建议: 将导入顺序调整为: 先导入所有 schema, 再导入所有 handler

Suggested change
import { SearchFilesSchema } from './schemas/queryFilesSchemas';
import { SearchSymbolsSchema } from './schemas/querySchemas';
import { SearchFilesSchema } from './schemas/queryFilesSchemas';
// 空行
import { handleSemanticSearch } from './handlers/semanticHandlers';
import { handleIndexRepo } from './handlers/indexHandlers';
import { handleSearchSymbols } from './handlers/queryHandlers';
import { handleSearchFiles } from './handlers/queryFilesHandlers';

import { handleSemanticSearch } from './handlers/semanticHandlers';
import { handleIndexRepo } from './handlers/indexHandlers';
import { handleSearchSymbols } from './handlers/queryHandlers';
import { handleSearchFiles } from './handlers/queryFilesHandlers';
import { CheckIndexSchema, StatusSchema } from './schemas/statusSchemas';
import { handleCheckIndex, handleStatus } from './handlers/statusHandlers';
import { PackIndexSchema, UnpackIndexSchema } from './schemas/archiveSchemas';
Expand Down Expand Up @@ -59,6 +61,10 @@ export const cliHandlers: Record<string, HandlerRegistration<any>> = {
schema: SearchSymbolsSchema,
handler: handleSearchSymbols,
},
'query-files': {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 NIT: 命令注册顺序与导入顺序不一致

新增的 'query-files' 命令放置在搜索相关命令之后,但导入语句放在最前面。这种不一致虽然不影响功能,但可能增加维护成本

建议: 保持命令注册顺序与模块逻辑分组一致,或调整导入顺序以匹配注册顺序

schema: SearchFilesSchema,
handler: handleSearchFiles,
},
'status': {
schema: StatusSchema,
handler: handleStatus,
Expand Down
20 changes: 20 additions & 0 deletions src/cli/schemas/queryFilesSchemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { z } from 'zod';

const languageEnum = z.enum(['auto', 'all', 'java', 'ts', 'python', 'go', 'rust', 'c', 'markdown', 'yaml']);
const searchModeEnum = z.enum(['substring', 'prefix', 'wildcard', 'regex', 'fuzzy']);

export const SearchFilesSchema = z.object({
pattern: z.string().min(1, 'Pattern is required'),
path: z.string().default('.'),
limit: z.coerce.number().int().positive().default(50),
mode: searchModeEnum.optional(),
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: Mode 字段缺少默认值

searchModeEnum 被设为 optional(),但没有提供默认值。当用户未指定 mode 时,mode 值为 undefined,可能导致后续搜索逻辑处理异常

建议: 为 mode 字段添加合理的默认值,例如 'substring'(最常用的搜索模式)

Suggested change
mode: searchModeEnum.optional(),
mode: searchModeEnum.default('substring')

caseInsensitive: z.boolean().default(false),
maxCandidates: z.coerce.number().int().positive().default(1000),
lang: languageEnum.default('auto'),
withRepoMap: z.boolean().default(false),
repoMapFiles: z.coerce.number().int().positive().default(20),
repoMapSymbols: z.coerce.number().int().positive().default(5),
wiki: z.string().default(''),
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 SUGGESTION: Wiki 字段类型不明确

wiki 字段使用空字符串作为默认值,但未在 languageEnum 中定义 'wiki' 语言。语义上 wiki 可能代表一种文件格式或需要单独验证其有效值列表

建议: 如果 wiki 是文件格式之一,应将其添加到 languageEnum;如果是其他用途(如维基页面 URL),考虑使用更有描述性的字段名如 wikiPath 或 wikiUrl

});

export type SearchFilesInput = z.infer<typeof SearchFilesSchema>;
2 changes: 2 additions & 0 deletions src/commands/ai.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Command } from 'commander';
import { indexCommand } from '../cli/commands/indexCommand.js';
import { queryCommand } from '../cli/commands/queryCommand.js';
import { queryFilesCommand } from '../cli/commands/queryFilesCommand.js';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ WARNING: 新增导入的模块文件可能不存在

导入了 queryFilesCommand,但无法确认 '../cli/commands/queryFilesCommand.js' 文件是否存在于项目中

建议: 请确保 '../cli/commands/queryFilesCommand.js' 文件已创建并正确导出 queryFilesCommand

import { semanticCommand } from '../cli/commands/semanticCommand.js';
import { serveCommand, agentCommand } from '../cli/commands/serveCommands.js';
import { packCommand, unpackCommand } from '../cli/commands/archiveCommands.js';
Expand All @@ -16,6 +17,7 @@ export const aiCommand = new Command('ai')
.addCommand(statusCommand)
.addCommand(repoMapCommand)
.addCommand(queryCommand)
.addCommand(queryFilesCommand)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 SUGGESTION: 新增子命令未提供描述

其他子命令(如 queryCommand、statusCommand)通常在各自文件中定义描述,此处仅注册命令

建议: 建议确认 queryFilesCommand 内部已正确配置 description 和相关选项,保持与其他命令的一致性

.addCommand(semanticCommand)
.addCommand(graphCommand)
.addCommand(packCommand)
Expand Down
Loading
Loading