feat(cli): add collection management commands
New collection subcommands: - show <name> Show collection details - update-cmd <name> [cmd] Set pre-update command (runs before indexing) - include <name> Include in default queries (default) - exclude <name> Exclude from default queries Collections with includeByDefault=false are skipped unless explicitly named with -c flag. CLI improvements: - 'qmd collection' shows help instead of error - 'qmd collection list' shows [excluded] tag - Better command descriptions and examples
This commit is contained in:
parent
07421de55f
commit
2b94e097d2
@ -25,10 +25,11 @@ export type ContextMap = Record<string, string>;
|
||||
* A single collection configuration
|
||||
*/
|
||||
export interface Collection {
|
||||
path: string; // Absolute path to index
|
||||
pattern: string; // Glob pattern (e.g., "**/*.md")
|
||||
context?: ContextMap; // Optional context definitions
|
||||
update?: string; // Optional bash command to run during qmd update
|
||||
path: string; // Absolute path to index
|
||||
pattern: string; // Glob pattern (e.g., "**/*.md")
|
||||
context?: ContextMap; // Optional context definitions
|
||||
update?: string; // Optional bash command to run during qmd update
|
||||
includeByDefault?: boolean; // Include in queries by default (default: true)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -169,6 +170,52 @@ export function listCollections(): NamedCollection[] {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get collections that are included by default in queries
|
||||
*/
|
||||
export function getDefaultCollections(): NamedCollection[] {
|
||||
return listCollections().filter(c => c.includeByDefault !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get collection names that are included by default
|
||||
*/
|
||||
export function getDefaultCollectionNames(): string[] {
|
||||
return getDefaultCollections().map(c => c.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a collection's settings
|
||||
*/
|
||||
export function updateCollectionSettings(
|
||||
name: string,
|
||||
settings: { update?: string | null; includeByDefault?: boolean }
|
||||
): boolean {
|
||||
const config = loadConfig();
|
||||
const collection = config.collections[name];
|
||||
if (!collection) return false;
|
||||
|
||||
if (settings.update !== undefined) {
|
||||
if (settings.update === null) {
|
||||
delete collection.update;
|
||||
} else {
|
||||
collection.update = settings.update;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.includeByDefault !== undefined) {
|
||||
if (settings.includeByDefault === true) {
|
||||
// true is default, remove the field
|
||||
delete collection.includeByDefault;
|
||||
} else {
|
||||
collection.includeByDefault = settings.includeByDefault;
|
||||
}
|
||||
}
|
||||
|
||||
saveConfig(config);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a collection
|
||||
*/
|
||||
|
||||
12
src/mcp.ts
12
src/mcp.ts
@ -23,7 +23,7 @@ import {
|
||||
DEFAULT_MULTI_GET_MAX_BYTES,
|
||||
} from "./store.js";
|
||||
import type { Store, StructuredSubSearch } from "./store.js";
|
||||
import { getCollection, getGlobalContext } from "./collections.js";
|
||||
import { getCollection, getGlobalContext, getDefaultCollectionNames } from "./collections.js";
|
||||
import { disposeDefaultLlamaCpp } from "./llm.js";
|
||||
|
||||
// =============================================================================
|
||||
@ -270,8 +270,11 @@ function createMcpServer(store: Store): McpServer {
|
||||
query: s.query,
|
||||
}));
|
||||
|
||||
// Use default collections if none specified
|
||||
const effectiveCollections = collections ?? getDefaultCollectionNames();
|
||||
|
||||
const results = await structuredSearch(store, subSearches, {
|
||||
collections,
|
||||
collections: effectiveCollections.length > 0 ? effectiveCollections : undefined,
|
||||
limit,
|
||||
minScore,
|
||||
});
|
||||
@ -578,8 +581,11 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole
|
||||
query: String(s.query || ""),
|
||||
}));
|
||||
|
||||
// Use default collections if none specified
|
||||
const effectiveCollections = params.collections ?? getDefaultCollectionNames();
|
||||
|
||||
const results = await structuredSearch(store, subSearches, {
|
||||
collections: params.collections,
|
||||
collections: effectiveCollections.length > 0 ? effectiveCollections : undefined,
|
||||
limit: params.limit ?? 10,
|
||||
minScore: params.minScore ?? 0,
|
||||
});
|
||||
|
||||
118
src/qmd.ts
118
src/qmd.ts
@ -1314,8 +1314,13 @@ function collectionList(): void {
|
||||
for (const coll of collections) {
|
||||
const updatedAt = coll.last_modified ? new Date(coll.last_modified) : new Date();
|
||||
const timeAgo = formatTimeAgo(updatedAt);
|
||||
|
||||
// Get YAML config to check includeByDefault
|
||||
const yamlColl = getCollectionFromYaml(coll.name);
|
||||
const excluded = yamlColl?.includeByDefault === false;
|
||||
const excludeTag = excluded ? ` ${c.yellow}[excluded]${c.reset}` : '';
|
||||
|
||||
console.log(`${c.cyan}${coll.name}${c.reset} ${c.dim}(qmd://${coll.name}/)${c.reset}`);
|
||||
console.log(`${c.cyan}${coll.name}${c.reset} ${c.dim}(qmd://${coll.name}/)${c.reset}${excludeTag}`);
|
||||
console.log(` ${c.dim}Pattern:${c.reset} ${coll.glob_pattern}`);
|
||||
console.log(` ${c.dim}Files:${c.reset} ${coll.active_count}`);
|
||||
console.log(` ${c.dim}Updated:${c.reset} ${timeAgo}`);
|
||||
@ -1915,7 +1920,12 @@ function outputResults(results: { file: string; displayPath: string; title: stri
|
||||
|
||||
// Resolve -c collection filter: supports single string, array, or undefined.
|
||||
// Returns validated collection names (exits on unknown collection).
|
||||
function resolveCollectionFilter(raw: string | string[] | undefined): string[] {
|
||||
function resolveCollectionFilter(raw: string | string[] | undefined, useDefaults: boolean = false): string[] {
|
||||
// If no filter specified and useDefaults is true, use default collections
|
||||
if (!raw && useDefaults) {
|
||||
const { getDefaultCollectionNames } = require("./collections.js");
|
||||
return getDefaultCollectionNames();
|
||||
}
|
||||
if (!raw) return [];
|
||||
const names = Array.isArray(raw) ? raw : [raw];
|
||||
const validated: string[] = [];
|
||||
@ -2003,7 +2013,8 @@ function search(query: string, opts: OutputOptions): void {
|
||||
const db = getDb();
|
||||
|
||||
// Validate collection filter (supports multiple -c flags)
|
||||
const collectionNames = resolveCollectionFilter(opts.collection);
|
||||
// Use default collections if none specified
|
||||
const collectionNames = resolveCollectionFilter(opts.collection, true);
|
||||
const singleCollection = collectionNames.length === 1 ? collectionNames[0] : undefined;
|
||||
|
||||
// Use large limit for --all, otherwise fetch more than needed and let outputResults filter
|
||||
@ -2057,7 +2068,8 @@ async function vectorSearch(query: string, opts: OutputOptions, _model: string =
|
||||
const store = getStore();
|
||||
|
||||
// Validate collection filter (supports multiple -c flags)
|
||||
const collectionNames = resolveCollectionFilter(opts.collection);
|
||||
// Use default collections if none specified
|
||||
const collectionNames = resolveCollectionFilter(opts.collection, true);
|
||||
const singleCollection = collectionNames.length === 1 ? collectionNames[0] : undefined;
|
||||
|
||||
checkIndexHealth(store.db);
|
||||
@ -2110,7 +2122,8 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri
|
||||
const store = getStore();
|
||||
|
||||
// Validate collection filter (supports multiple -c flags)
|
||||
const collectionNames = resolveCollectionFilter(opts.collection);
|
||||
// Use default collections if none specified
|
||||
const collectionNames = resolveCollectionFilter(opts.collection, true);
|
||||
const singleCollection = collectionNames.length === 1 ? collectionNames[0] : undefined;
|
||||
|
||||
checkIndexHealth(store.db);
|
||||
@ -2535,9 +2548,102 @@ if (fileURLToPath(import.meta.url) === process.argv[1] || process.argv[1]?.endsW
|
||||
break;
|
||||
}
|
||||
|
||||
case "set-update":
|
||||
case "update-cmd": {
|
||||
const name = cli.args[1];
|
||||
const cmd = cli.args.slice(2).join(' ') || null;
|
||||
if (!name) {
|
||||
console.error("Usage: qmd collection update-cmd <name> [command]");
|
||||
console.error(" Set the command to run before indexing (e.g., 'git pull')");
|
||||
console.error(" Omit command to clear it");
|
||||
process.exit(1);
|
||||
}
|
||||
const { updateCollectionSettings, getCollection } = await import("./collections.js");
|
||||
const col = getCollection(name);
|
||||
if (!col) {
|
||||
console.error(`Collection not found: ${name}`);
|
||||
process.exit(1);
|
||||
}
|
||||
updateCollectionSettings(name, { update: cmd });
|
||||
if (cmd) {
|
||||
console.log(`✓ Set update command for '${name}': ${cmd}`);
|
||||
} else {
|
||||
console.log(`✓ Cleared update command for '${name}'`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "include":
|
||||
case "exclude": {
|
||||
const name = cli.args[1];
|
||||
if (!name) {
|
||||
console.error(`Usage: qmd collection ${subcommand} <name>`);
|
||||
console.error(` ${subcommand === 'include' ? 'Include' : 'Exclude'} collection in default queries`);
|
||||
process.exit(1);
|
||||
}
|
||||
const { updateCollectionSettings, getCollection } = await import("./collections.js");
|
||||
const col = getCollection(name);
|
||||
if (!col) {
|
||||
console.error(`Collection not found: ${name}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const include = subcommand === 'include';
|
||||
updateCollectionSettings(name, { includeByDefault: include });
|
||||
console.log(`✓ Collection '${name}' ${include ? 'included in' : 'excluded from'} default queries`);
|
||||
break;
|
||||
}
|
||||
|
||||
case "show":
|
||||
case "info": {
|
||||
const name = cli.args[1];
|
||||
if (!name) {
|
||||
console.error("Usage: qmd collection show <name>");
|
||||
process.exit(1);
|
||||
}
|
||||
const { getCollection } = await import("./collections.js");
|
||||
const col = getCollection(name);
|
||||
if (!col) {
|
||||
console.error(`Collection not found: ${name}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Collection: ${name}`);
|
||||
console.log(` Path: ${col.path}`);
|
||||
console.log(` Pattern: ${col.pattern}`);
|
||||
console.log(` Include: ${col.includeByDefault !== false ? 'yes (default)' : 'no'}`);
|
||||
if (col.update) {
|
||||
console.log(` Update: ${col.update}`);
|
||||
}
|
||||
if (col.context) {
|
||||
const ctxCount = Object.keys(col.context).length;
|
||||
console.log(` Contexts: ${ctxCount}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "help":
|
||||
case undefined: {
|
||||
console.log("Usage: qmd collection <command> [options]");
|
||||
console.log("");
|
||||
console.log("Commands:");
|
||||
console.log(" list List all collections");
|
||||
console.log(" add <path> [--name NAME] Add a collection");
|
||||
console.log(" remove <name> Remove a collection");
|
||||
console.log(" rename <old> <new> Rename a collection");
|
||||
console.log(" show <name> Show collection details");
|
||||
console.log(" update-cmd <name> [cmd] Set pre-update command (e.g., 'git pull')");
|
||||
console.log(" include <name> Include in default queries");
|
||||
console.log(" exclude <name> Exclude from default queries");
|
||||
console.log("");
|
||||
console.log("Examples:");
|
||||
console.log(" qmd collection add ~/notes --name notes");
|
||||
console.log(" qmd collection update-cmd brain 'git pull'");
|
||||
console.log(" qmd collection exclude archive");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`Unknown subcommand: ${subcommand}`);
|
||||
console.error("Available: list, add, remove, rename");
|
||||
console.error("Run 'qmd collection help' for usage");
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user