Files
gstack/bin/gstack-code-intelligence
T
SinabinaandClaude Opus 4.8 6686202be9 fix: persist indexed repo per provider and detect installs honestly
search was running in cwd and missing the repo you indexed. Persist the indexed
path per provider in the selection store and resolve it back so `search` reads
the same graph `index` built. Graphify availability now checks `graphify --version`
(installed = selectable) instead of "a graph already exists here", and the CLI
keys Graphify sources on the repo path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 17:40:50 -07:00

165 lines
6.4 KiB
TypeScript
Executable File

#!/usr/bin/env bun
/**
* gstack-code-intelligence — pick a code-intelligence provider and use it to
* index and search this repo. OPTIONAL: with nothing selected, gstack works
* fine and callers use grep / the file-only decision store.
*
* Usage:
* gstack-code-intelligence options # list providers (GBrain first) + availability
* gstack-code-intelligence status # current selection + availability
* gstack-code-intelligence select <gbrain|sourcebot|graphify|none>
* gstack-code-intelligence consent [repo-path] # allow indexing this repo (per-repo)
* gstack-code-intelligence index [repo-path] # index the repo with the selected provider
* gstack-code-intelligence search <query...> # search via the selected provider
*
* Non-local providers (GBrain, or a Sourcebot on a remote host) refuse to index
* until you consent for that repo. Graphify and a localhost Sourcebot are local:
* nothing leaves the machine, so no consent is needed. Graphify is never
* auto-installed.
*/
import { basename, resolve } from "path";
import {
CodeProviderError,
RECOMMENDED_ORDER,
detectAvailable,
hasConsent,
providerById,
readSelection,
resolveSelectedProvider,
setConsent,
setProvider,
setRoot,
type CodeProviderId,
} from "../lib/code-intelligence";
const PROVIDER_IDS = new Set<CodeProviderId>(["gbrain", "sourcebot", "graphify"]);
const LABEL: Record<CodeProviderId, string> = { gbrain: "GBrain", sourcebot: "Sourcebot", graphify: "Graphify" };
const NOTE: Record<CodeProviderId, string> = {
gbrain: "recommended; federated memory + code (sends content to your GBrain DB)",
sourcebot: "self-hosted whole-repo regex search (local when on localhost)",
graphify: "local tree-sitter code graph, nothing leaves the machine (install it yourself)",
};
function out(s: string): void {
process.stdout.write(`${s}\n`);
}
function fail(s: string): never {
process.stderr.write(`gstack-code-intelligence: ${s}\n`);
process.exit(1);
}
async function cmdOptions(): Promise<void> {
out("Code-intelligence providers (indexing is optional; GBrain recommended):\n");
const avail = await detectAvailable();
const byId = new Map(avail.map((a) => [a.id, a]));
for (const id of RECOMMENDED_ORDER) {
const a = byId.get(id);
const mark = a?.available ? "available" : "not available";
out(` ${id === "gbrain" ? "*" : " "} ${LABEL[id].padEnd(10)} [${mark}] — ${NOTE[id]}`);
if (a?.detail) out(` ${a.detail}`);
}
out("\nSelect one with: gstack-code-intelligence select <provider>");
}
async function cmdStatus(): Promise<void> {
const sel = readSelection();
out(`selected: ${sel.provider ?? "none (grep / file-only fallback)"}`);
const avail = await detectAvailable();
for (const a of avail) out(` ${LABEL[a.id]}: ${a.available ? "available" : "unavailable"} (${a.detail})`);
}
function cmdSelect(arg: string | undefined): void {
if (arg === "none") {
setProvider(null);
out("code-intelligence provider cleared; gstack uses grep / file-only fallback");
return;
}
if (!arg || !PROVIDER_IDS.has(arg as CodeProviderId)) {
fail("Usage: select <gbrain|sourcebot|graphify|none>");
}
const id = arg as CodeProviderId;
setProvider(id);
out(`selected ${LABEL[id]}.`);
const provider = providerById(id);
if (!provider.local) out(`${LABEL[id]} sends repo content off this machine — run \`consent\` in a repo before indexing it.`);
}
function cmdConsent(pathArg: string | undefined): void {
const repoPath = resolve(pathArg ?? process.cwd());
setConsent(repoPath, true);
out(`indexing consent recorded for ${repoPath}`);
}
async function cmdIndex(pathArg: string | undefined): Promise<void> {
const provider = resolveSelectedProvider();
if (!provider) fail("no provider selected; run `select <provider>` first");
const repoPath = resolve(pathArg ?? process.cwd());
const consented = hasConsent(repoPath);
if (!provider!.local && !consented) {
fail(`${provider!.label} would send this repo's content off the machine. Run \`gstack-code-intelligence consent ${repoPath}\` first.`);
}
// Graphify keys sources on the repo path; GBrain/Sourcebot on a short id.
const sourceId = provider!.id === "graphify" ? repoPath : basename(repoPath);
const repo = { id: sourceId, path: repoPath };
try {
const registered = await provider!.registerSource(repo, { consented });
out(`registered ${repo.id} with ${provider!.label} (${registered.state})`);
const refreshed = await provider!.refresh({ id: registered.id }, { consented });
// Remember which repo this provider indexed so `search` reads the same graph.
setRoot(provider!.id, repoPath);
out(`indexed: ${refreshed.state}${refreshed.itemCount != null ? ` (${refreshed.itemCount} items)` : ""}`);
} catch (err) {
handleProviderError(err, provider!.label);
}
}
async function cmdSearch(terms: string[]): Promise<void> {
const query = terms.join(" ").trim();
if (!query) fail("Usage: search <query...>");
const provider = resolveSelectedProvider();
if (!provider) fail("no provider selected; run `select <provider>` first (or use grep)");
try {
const hits = await provider!.search(query, { limit: 10 });
if (!hits.length) {
out("(no results)");
return;
}
for (const h of hits) out(`${h.score != null ? `[${h.score.toFixed(2)}] ` : ""}${h.ref}${h.snippet ? ` — ${h.snippet}` : ""}`);
} catch (err) {
handleProviderError(err, provider!.label);
}
}
function handleProviderError(err: unknown, label: string): never {
if (err instanceof CodeProviderError) {
if (err.code === "PROVIDER_UNAVAILABLE") {
fail(`${label} is unavailable (${err.message}). gstack still works — fall back to grep / file-only.`);
}
fail(`${label} ${err.code}: ${err.message}`);
}
fail(err instanceof Error ? err.message : String(err));
}
async function main(): Promise<void> {
const [action, ...rest] = process.argv.slice(2);
switch (action) {
case "options":
return cmdOptions();
case "status":
return cmdStatus();
case "select":
return cmdSelect(rest[0]);
case "consent":
return cmdConsent(rest[0]);
case "index":
return cmdIndex(rest[0]);
case "search":
return cmdSearch(rest);
default:
fail("Usage: options | status | select <provider> | consent [path] | index [path] | search <query...>");
}
}
main().catch((err) => fail(err instanceof Error ? err.message : String(err)));