fix: correct all three adapters to real tool interfaces (found by live testing)

Parallel real-environment tests (graphify 0.9.23, Sourcebot v5 in Docker,
gbrain 0.42.56) surfaced real mismatches:

- graphify: build via `graphify update <dir>` (the local, no-LLM path) instead
  of `graphify <dir>` (which runs an LLM backend needing a key + network, so the
  old path wasn't actually local); query via `graphify query --graph <graph.json>`
  so it reads the indexed graph regardless of cwd; parse the real NODE/EDGE output
  (file lives at src=/at=) instead of an invented format.
- sourcebot: Sourcebot v5 gates /api/search behind auth — send
  `Authorization: Bearer <SOURCEBOT_API_KEY>`; treat 401/403 as PROVIDER_UNAVAILABLE;
  make status probe /api/search without following the login redirect.
- gbrain: degrade engine/DB init failures (e.g. pglite WASM, garrytan/gbrain#223)
  to PROVIDER_UNAVAILABLE with a one-line message instead of PROVIDER_ERROR + a raw
  stack dump; drop flags the real CLI doesn't define (`sync --strategy`,
  `search --source`); align put/delete to stdin, export to brain-wide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sinabina
2026-07-21 17:40:50 -07:00
co-authored by Claude Opus 4.8
parent 9650e8b056
commit f0f13998d8
3 changed files with 120 additions and 51 deletions
+26 -2
View File
@@ -45,6 +45,12 @@ export interface SourcebotOptions {
baseUrl?: string;
/** Path to the server's config.json (for register_source). Defaults to SOURCEBOT_CONFIG. */
configPath?: string;
/**
* API key for the Sourcebot REST API. Defaults to SOURCEBOT_API_KEY. Sourcebot
* v5 gates `/api/search` behind auth (`Authorization: Bearer <key>`); without
* it, `search` gets HTTP 401. Generate one in Settings -> API Keys.
*/
apiKey?: string;
/** Injectable fetch for tests. */
fetch?: FetchLike;
env?: NodeJS.ProcessEnv;
@@ -66,17 +72,23 @@ export class SourcebotProvider implements CodeProvider {
readonly local: boolean;
readonly #baseUrl: string;
readonly #configPath?: string;
readonly #apiKey?: string;
readonly #fetch: FetchLike;
constructor(opts: SourcebotOptions = {}) {
const env = opts.env ?? process.env;
this.#baseUrl = (opts.baseUrl ?? env.SOURCEBOT_URL ?? DEFAULT_URL).replace(/\/$/, "");
this.#configPath = opts.configPath ?? env.SOURCEBOT_CONFIG;
this.#apiKey = opts.apiKey ?? env.SOURCEBOT_API_KEY;
this.#fetch = opts.fetch ?? globalThis.fetch;
this.local = isLoopback(this.#baseUrl);
assertRequiredCapabilities(this.id, this.capabilities);
}
#authHeaders(): Record<string, string> {
return this.#apiKey ? { Authorization: `Bearer ${this.#apiKey}` } : {};
}
has(capability: CodeProviderCapability): boolean {
return this.capabilities.has(capability);
}
@@ -127,8 +139,17 @@ export class SourcebotProvider implements CodeProvider {
}
async status(_source?: SourceRef, opts: OpOptions = {}): Promise<SourceStatus> {
// `redirect: manual` so an auth-gated server (307 -> /login) reads as
// not-usable instead of following to a 200 and falsely reporting "ready".
try {
const res = await this.#fetchWithTimeout(this.#baseUrl, { method: "GET" }, opts.timeout ?? DEFAULT_TIMEOUT_MS);
const res = await this.#fetchWithTimeout(
`${this.#baseUrl}/api/search`,
{ method: "POST", headers: { "Content-Type": "application/json", ...this.#authHeaders() }, body: JSON.stringify({ query: "sourcebot", matches: 1, isRegexEnabled: false }), redirect: "manual" },
opts.timeout ?? DEFAULT_TIMEOUT_MS,
);
if (res.status === 401 || res.status === 403) {
return { id: "*", state: "unknown", partial: true, detail: "reachable but not authenticated (set SOURCEBOT_API_KEY)" };
}
return { id: "*", state: res.ok ? "ready" : "unknown", partial: true, detail: `HTTP ${res.status}` };
} catch {
return { id: "*", state: "unknown", partial: true, detail: `unreachable at ${this.#baseUrl}` };
@@ -140,12 +161,15 @@ export class SourcebotProvider implements CodeProvider {
try {
res = await this.#fetchWithTimeout(`${this.#baseUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
headers: { "Content-Type": "application/json", Accept: "application/json", ...this.#authHeaders() },
body: JSON.stringify(body),
}, timeout);
} catch (err) {
throw new CodeProviderError("PROVIDER_UNAVAILABLE", `Sourcebot unreachable at ${this.#baseUrl}: ${(err as Error).message}`, this.id);
}
if (res.status === 401 || res.status === 403) {
throw new CodeProviderError("PROVIDER_UNAVAILABLE", `Sourcebot requires authentication; set SOURCEBOT_API_KEY (HTTP ${res.status})`, this.id);
}
if (!res.ok) throw new CodeProviderError("PROVIDER_ERROR", `Sourcebot ${path} returned HTTP ${res.status}`, this.id);
try {
return await res.json();