garrytan/gstack · error · Error

gbrain sources list returned non-JSON output: ${(err as Erro

Error message

gbrain sources list returned non-JSON output: ${(err as Error).message}

What it means

Thrown by probeSource() when gbrain sources list --json exits successfully and prints to stdout, but JSON.parse(stdout) throws. It captures the parse error message so the developer can see what tripped the parser. This is distinct from 316 (no binary), 317 (DB/config), and 315 (no JSON emitted at all) — here stdout exists but is not valid JSON.

Source

Thrown at lib/gbrain-sources.ts:143

      shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
    });
  } catch (err) {
    const e = err as NodeJS.ErrnoException & { stderr?: Buffer };
    const stderr = e.stderr?.toString() || "";
    if (e.code === "ENOENT" || stderr.includes("command not found")) {
      throw new Error("gbrain CLI not on PATH");
    }
    if (stderr.includes("Cannot connect to database") || stderr.includes("config.json")) {
      throw new Error("gbrain not configured (run /setup-gbrain)");
    }
    throw err;
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(stdout);
  } catch (err) {
    throw new Error(`gbrain sources list returned non-JSON output: ${(err as Error).message}`);
  }

  const sources = parseSourcesList(parsed);
  const match = sources.find((s) => s.id === id);
  if (!match) return { status: "absent" };
  return {
    status: "match",
    registered_path: match.local_path,
  };
}

/**
 * Ensure source <id> is registered at <path>. Idempotent.
 *
 * Behavior:
 *   - status=absent  → `gbrain sources add <id> --path <path> [--federated]`, returns changed=true.
 *   - status=match + same path → no-op, returns changed=false.
 *   - status=match + different path → `sources remove --confirm-destructive` + `sources add`, returns changed=true.

View on GitHub (pinned to 94993f7401)

Solutions

  1. Run `gbrain sources list --json` manually and inspect the raw stdout to see what preceded the JSON.
  2. Upgrade gbrain to a version that emits clean JSON on stdout with logs on stderr.
  3. If a log line is the culprit, configure gbrain's log level to suppress stdout logging.
  4. Pipe through a JSON extractor if a workaround is needed before upgrading.
Defensive patterns

Strategy: try-catch

Validate before calling

import { execFileSync } from 'child_process';
function gbrainStdoutIsJson(env: NodeJS.ProcessEnv = process.env): boolean {
  try {
    const out = execFileSync('gbrain', ['sources', 'list', '--json'], { encoding: 'utf-8', env, timeout: 5_000 });
    JSON.parse(out); return true;
  } catch { return false; }
}

Try / catch

try {
  return probeSource(id, env);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('gbrain sources list returned non-JSON')) {
    // Upgrade gbrain or redirect its logs off stdout, then retry
    throw new Error('gbrain emitted non-JSON stdout. Upgrade gbrain and retry.');
  }
  throw e;
}

Prevention

When it happens

Trigger: gbrain sources list --json exits 0 with stdout containing non-JSON text (e.g. a log line before the JSON, a warning, an incomplete buffer, or a human-format table because --json was ignored). JSON.parse throws and the catch wraps it.

Common situations: gbrain version that prints a deprecation/log line to stdout before the JSON; --json flag ignored in an older version so human text is emitted; stdout truncated by a buffer limit or pipe closure; a plugin injected non-JSON output.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/57c7dbe50b271518. Report an issue: GitHub.