refactor(extension): auto-index on tool call and skip registration when binary missing

- Only register tool if `codexis` binary is in PATH
- Run incremental index via `pi.exec` on each tool call to keep DB fresh
- Remove `findDatabase` helper; derive DB path from git root directly
- Replace `defineTool` with inline `pi.registerTool` call
- Update imports (`@sinclair/typebox`, lazy `execSync`)
- Fix output flag help text in main.go
This commit is contained in:
2026-04-10 15:56:47 -04:00
parent 39fcfc2968
commit dfd61f899a
2 changed files with 119 additions and 93 deletions

View File

@@ -3,11 +3,14 @@
* *
* Provides a single tool that queries the .codexis/index.db SQLite database * Provides a single tool that queries the .codexis/index.db SQLite database
* containing symbols, files, and line numbers for the codebase. * containing symbols, files, and line numbers for the codebase.
*
* - Only registers if `codexis` binary is in PATH
* - Auto-indexes on first tool call if DB is missing
* - Re-indexes (incremental) on each call to keep index fresh
*/ */
import { Type } from "@mariozechner/pi-ai"; import { Type } from "@sinclair/typebox";
import { defineTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { execSync } from "node:child_process";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
@@ -56,6 +59,7 @@ Example queries:
function findGitRoot(cwd: string): string | null { function findGitRoot(cwd: string): string | null {
try { try {
const { execSync } = require("node:child_process");
return execSync("git rev-parse --show-toplevel", { return execSync("git rev-parse --show-toplevel", {
cwd, cwd,
encoding: "utf-8", encoding: "utf-8",
@@ -66,16 +70,23 @@ function findGitRoot(cwd: string): string | null {
} }
} }
function findDatabase(cwd: string): string | null { function codexisAvailable(): boolean {
const gitRoot = findGitRoot(cwd); try {
if (!gitRoot) return null; const { execSync } = require("node:child_process");
const dbPath = join(gitRoot, ".codexis", "index.db"); execSync("which codexis", { stdio: ["pipe", "pipe", "pipe"] });
if (!existsSync(dbPath)) return null; return true;
return dbPath; } catch {
return false;
}
} }
const codexisTool = defineTool({ export default function (pi: ExtensionAPI) {
name: "codexis", if (!codexisAvailable()) {
return;
}
pi.registerTool({
name: "Codexis",
label: "Codexis", label: "Codexis",
description: DESCRIPTION, description: DESCRIPTION,
parameters: Type.Object({ parameters: Type.Object({
@@ -84,11 +95,29 @@ const codexisTool = defineTool({
}), }),
}), }),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) { async execute(_toolCallId, params, signal, _onUpdate, ctx) {
const dbPath = findDatabase(ctx.cwd); const gitRoot = findGitRoot(ctx.cwd);
if (!dbPath) { if (!gitRoot) {
throw new Error("Not in a git repository");
}
const dbPath = join(gitRoot, ".codexis", "index.db");
// Run incremental index to keep DB fresh (fast if nothing changed)
const indexResult = await pi.exec("codexis", [gitRoot], {
signal,
timeout: 120_000,
});
if (indexResult.code !== 0) {
throw new Error( throw new Error(
"No code index found. Run `codexis` in the repo root to generate .codexis/index.db" `codexis indexing failed (exit ${indexResult.code}): ${indexResult.stderr}`,
);
}
if (!existsSync(dbPath)) {
throw new Error(
"codexis ran but no index.db was produced. Check codexis output.",
); );
} }
@@ -102,7 +131,9 @@ const codexisTool = defineTool({
!normalized.startsWith("EXPLAIN") && !normalized.startsWith("EXPLAIN") &&
!normalized.startsWith("PRAGMA") !normalized.startsWith("PRAGMA")
) { ) {
throw new Error("Only SELECT, WITH, EXPLAIN, and PRAGMA queries are allowed"); throw new Error(
"Only SELECT, WITH, EXPLAIN, and PRAGMA queries are allowed",
);
} }
const stmt = db.prepare(params.sql); const stmt = db.prepare(params.sql);
@@ -123,7 +154,7 @@ const codexisTool = defineTool({
}); });
const widths = columns.map((col, i) => const widths = columns.map((col, i) =>
Math.max(col.length, ...data.map((row) => row[i].length)) Math.max(col.length, ...data.map((row) => row[i].length)),
); );
const header = columns const header = columns
@@ -131,9 +162,7 @@ const codexisTool = defineTool({
.join(" "); .join(" ");
const separator = widths.map((w) => "-".repeat(w)).join(" "); const separator = widths.map((w) => "-".repeat(w)).join(" ");
const body = data const body = data
.map((row) => .map((row) => row.map((val, i) => val.padEnd(widths[i])).join(" "))
row.map((val, i) => val.padEnd(widths[i])).join(" ")
)
.join("\n"); .join("\n");
const result = `${header}\n${separator}\n${body}`; const result = `${header}\n${separator}\n${body}`;
@@ -154,8 +183,5 @@ const codexisTool = defineTool({
db.close(); db.close();
} }
}, },
}); });
export default function (pi: ExtensionAPI) {
pi.registerTool(codexisTool);
} }

View File

@@ -20,7 +20,7 @@ const dbFileName = "index.db"
func main() { func main() {
force := flag.Bool("force", false, "Force full re-index (ignore file hashes)") force := flag.Bool("force", false, "Force full re-index (ignore file hashes)")
output := flag.String("o", "", "Output database path (default: <root>/.codexis.db)") output := flag.String("o", "", "Output database path (default: <root>/.codexis/index.db)")
flag.Parse() flag.Parse()
root := "." root := "."