src / tools.ts
import { tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { configSchematics } from "./config";
import { listSources, reindex, search } from "./store";
export async function toolsProvider(ctl: ToolsProviderController) {
const getConfig = () => ctl.getPluginConfig(configSchematics);
return [
tool({
name: "alexiel_rag_reindex",
description:
"Re-scan the folders configured in this plugin's settings (Allowed Folders) and rebuild " +
"the search index from their current contents. Read-only — never writes, deletes, or " +
"executes anything outside the plugin's own database. Call this when the user says their " +
"files have changed and they want the index refreshed, or when starting a task that " +
"depends on up-to-date grounding.",
parameters: {},
implementation: async () => {
const config = getConfig();
const result = reindex(
config.get("allowedFolders"),
config.get("fileExtensions"),
config.get("chunkSize"),
config.get("chunkOverlap"),
config.get("maxFileSizeBytes"),
);
if (result.folders.length === 0) {
return "No folders configured. Set 'Allowed Folders' in this plugin's settings first.";
}
return (
`Indexed ${result.filesIndexed} files into ${result.chunksIndexed} chunks ` +
`(${result.filesSkipped} files skipped) from:\n${result.folders.join("\n")}`
);
},
}),
tool({
name: "alexiel_rag_search",
description:
"Search the indexed folders for content relevant to a query. Every result is tagged with " +
"its exact source file path and chunk number — treat that as the citation for anything you " +
"state based on these results. If nothing relevant comes back, say so rather than guessing.",
parameters: {
query: z.string().min(1).max(300).describe("Search terms"),
limit: z.number().int().min(1).max(20).optional().describe("Max results (default 5)"),
},
implementation: async ({ query, limit }) => {
const results = search(query, limit ?? 5);
if (results.length === 0) {
return `No indexed content matches "${query}". (Run alexiel_rag_reindex if the index may be stale or empty.)`;
}
return results
.map((r) => `[${r.file_path} #chunk${r.chunk_index}]\n${r.content}`)
.join("\n\n---\n\n");
},
}),
tool({
name: "alexiel_rag_list_sources",
description:
"List every file currently in the index, with how many chunks each contributed. Use this " +
"to check what's actually grounded before trusting a search result, or to show the user " +
"what's indexed.",
parameters: {},
implementation: async () => {
const sources = listSources();
if (sources.length === 0) return "Index is empty. Run alexiel_rag_reindex first.";
return sources.map((s) => `${s.file_path} (${s.chunks} chunks)`).join("\n");
},
}),
];
}