Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions apps/web/src/app/tui.json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextResponse } from "next/server";

/**
* Serves the Kilo TUI keybind config JSON Schema at `app.kilo.ai/tui.json`.
*
* Fetches the upstream opencode schema at request time. Cached at the CDN edge
* for 1 hour with stale-while-revalidate, so the actual upstream fetch happens
* at most once per hour per region.
*
* The URL is referenced as `$schema` in generated `tui.json` files by the
* Kilo CLI (`packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts`) and in
* the keybinds documentation at `https://app.kilo.ai/docs/customize/keybinds`.
*/

const UPSTREAM = "https://opencode.ai/tui.json";
const CACHE_SECONDS = 60 * 60; // 1 hour

export async function GET() {
const res = await fetch(UPSTREAM, { next: { revalidate: CACHE_SECONDS } });
if (!res.ok) {
return NextResponse.json(
{ error: `upstream ${UPSTREAM} returned ${res.status}` },
{ status: 502 },
);
}
const upstream: unknown = await res.json();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Unhandled JSON.parse failure — if the upstream returns a 200 with a non-JSON body (e.g. a CloudFlare HTML error page), res.json() will throw and the route will return an unhandled 500 instead of a graceful 502.

Consider wrapping:

let upstream: unknown;
try {
  upstream = await res.json();
} catch {
  return NextResponse.json(
    { error: `upstream ${UPSTREAM} returned invalid JSON` },
    { status: 502 },
  );
}

Note: config.json/route.ts has the same gap, so this pattern could be addressed in both places.


return NextResponse.json(upstream, {
headers: {
"cache-control": `public, max-age=0, s-maxage=${CACHE_SECONDS}, stale-while-revalidate=${CACHE_SECONDS}`,
"access-control-allow-origin": "*",
},
});
}
Loading