From 378f9d6b892c81d2eca146a4ebcb43999572093b Mon Sep 17 00:00:00 2001 From: filiksyos Date: Sun, 12 Apr 2026 07:44:30 +0300 Subject: [PATCH 01/37] chore(views): require VIEWS_IP_SALT in production, document in .env.example --- .env.example | 7 +++---- app/api/increment-views/route.ts | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 6a84120..67c4fcd 100644 --- a/.env.example +++ b/.env.example @@ -21,7 +21,6 @@ OPENROUTER_API_KEY= # Cache TTL for reverse-prompt results in Supabase (hours; default 24) # CACHE_TTL_HOURS=24 -# Optional secret salt for hashing visitor IPs in the view counter. -# Generate one with: openssl rand -hex 32 -# If unset, a public fallback is used and a warning is logged in production. -# VIEWS_IP_SALT= +# REQUIRED in production — generate with: openssl rand -hex 32 (or PowerShell random hex) +# Without this, the app will refuse to start in production. +VIEWS_IP_SALT= diff --git a/app/api/increment-views/route.ts b/app/api/increment-views/route.ts index bc06c19..7a2a807 100644 --- a/app/api/increment-views/route.ts +++ b/app/api/increment-views/route.ts @@ -21,9 +21,9 @@ if ( IP_HASH_SALT === DEFAULT_IP_HASH_SALT && process.env.NODE_ENV === "production" ) { - console.warn( - "[increment-views] VIEWS_IP_SALT is not set — falling back to the public default. " + - "Set a random secret in your deployment env for production use." + throw new Error( + "[increment-views] VIEWS_IP_SALT is not set. " + + "Set a random secret (openssl rand -hex 32) in your deployment env." ); } From 3c70ed4c9ccf8c4ba6494eb36ac67488a268c4c9 Mon Sep 17 00:00:00 2001 From: filiksyos Date: Mon, 13 Apr 2026 12:38:36 +0300 Subject: [PATCH 02/37] whatever --- app/api/increment-views/route.ts | 5 ----- components/reverse-prompt-home.tsx | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/api/increment-views/route.ts b/app/api/increment-views/route.ts index 7a2a807..ad6947b 100644 --- a/app/api/increment-views/route.ts +++ b/app/api/increment-views/route.ts @@ -1,6 +1,5 @@ import { createHash } from "crypto"; import { NextRequest, NextResponse } from "next/server"; -import { isHomeExampleRepo } from "@/lib/home-example-repos"; import { isValidGitHubRepoPath, normalizeRepoSegment, @@ -95,10 +94,6 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Invalid owner or repo." }, { status: 400 }); } - if (isHomeExampleRepo(owner, repo)) { - return NextResponse.json({ ok: true }); - } - const supabase = getSupabase(); if (!supabase) { return NextResponse.json({ error: "Database unavailable." }, { status: 503 }); diff --git a/components/reverse-prompt-home.tsx b/components/reverse-prompt-home.tsx index 476fa55..bdaaae2 100644 --- a/components/reverse-prompt-home.tsx +++ b/components/reverse-prompt-home.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; -import { HOME_EXAMPLES, isHomeExampleRepo } from "@/lib/home-example-repos"; +import { HOME_EXAMPLES } from "@/lib/home-example-repos"; import { parseGitHubRepoInput } from "@/lib/parse-github-repo"; const GITREVERSE_HISTORY_KEY = "gitreverse_history"; @@ -107,7 +107,6 @@ export function ReversePromptHome({ const o = owner?.trim(); const r = repo?.trim(); if (!o || !r) return; - if (isHomeExampleRepo(o, r)) return; /* Server-side dedupes by IP hash, so we no longer need a localStorage gate. */ void fetch("/api/increment-views", { From 88b8107a97d81f345481d965fadacd47c424213c Mon Sep 17 00:00:00 2001 From: filiksyos Date: Mon, 13 Apr 2026 13:32:09 +0300 Subject: [PATCH 03/37] removed cache ttl --- .env.example | 3 --- app/api/reverse-prompt/route.ts | 22 ++-------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index 67c4fcd..016718b 100644 --- a/.env.example +++ b/.env.example @@ -18,9 +18,6 @@ OPENROUTER_API_KEY= # OPENROUTER_HTTP_REFERER=https://yoursite.example # OPENROUTER_APP_TITLE=gitreverse -# Cache TTL for reverse-prompt results in Supabase (hours; default 24) -# CACHE_TTL_HOURS=24 - # REQUIRED in production — generate with: openssl rand -hex 32 (or PowerShell random hex) # Without this, the app will refuse to start in production. VIEWS_IP_SALT= diff --git a/app/api/reverse-prompt/route.ts b/app/api/reverse-prompt/route.ts index 8238158..a43a304 100644 --- a/app/api/reverse-prompt/route.ts +++ b/app/api/reverse-prompt/route.ts @@ -81,11 +81,6 @@ function buildUserMessage( ].join("\n"); } -function cacheTtlHours(): number { - const n = Number(process.env.CACHE_TTL_HOURS); - return Number.isFinite(n) && n > 0 ? n : 24; -} - /** Maps to client 429 handling → “Browse the library” (same as GitHub/rate limits). */ function isExhaustedCreditsOrQuotaMessage(msg: string): boolean { const lower = msg.toLowerCase(); @@ -187,26 +182,16 @@ export async function POST(request: NextRequest) { const promise = (async () => { const supabase = getSupabase(); - let stalePrompt: string | null = null; if (supabase) { try { - const ttlHours = cacheTtlHours(); const { data, error } = await supabase .from("prompt_cache") - .select("prompt, cached_at") + .select("prompt") .eq("owner", owner) .eq("repo", repo) .maybeSingle(); if (!error && data?.prompt) { - if (data.cached_at) { - const ageHours = - (Date.now() - new Date(data.cached_at).getTime()) / 36e5; - if (ageHours < ttlHours) { - return { prompt: data.prompt as string }; - } - } - // Entry exists but is stale — keep as fallback - stalePrompt = data.prompt as string; + return { prompt: data.prompt as string }; } } catch { // cache miss — continue to GitHub + LLM @@ -319,9 +304,6 @@ export async function POST(request: NextRequest) { isExhaustedCreditsOrQuotaMessage(msg); if (creditsExhausted) { - if (stalePrompt) { - return { prompt: stalePrompt }; - } return NextResponse.json( { error: "Service is currently over capacity. Try again later." }, { status: 429 } From ea0c8c096c66ec4839fce1618edabd90e9430f3f Mon Sep 17 00:00:00 2001 From: filiksyos Date: Mon, 13 Apr 2026 14:11:18 +0300 Subject: [PATCH 04/37] whatever --- components/library-page.tsx | 41 ++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/components/library-page.tsx b/components/library-page.tsx index 606ff5a..760b470 100644 --- a/components/library-page.tsx +++ b/components/library-page.tsx @@ -329,30 +329,25 @@ export function LibraryPage({ initialData, initialTotal }: LibraryPageProps) { ); From ca1a783543c7c411b4fd7b3260147c178ffb6891 Mon Sep 17 00:00:00 2001 From: filiksyos Date: Tue, 14 Apr 2026 14:54:00 +0300 Subject: [PATCH 05/37] custom reverse --- .env.example | 4 + README.md | 8 ++ app/api/custom-reverse/route.ts | 104 ++++++++++++++++ components/reverse-prompt-home.tsx | 194 +++++++++++++++++++++-------- 4 files changed, 257 insertions(+), 53 deletions(-) create mode 100644 app/api/custom-reverse/route.ts diff --git a/.env.example b/.env.example index 016718b..71ef094 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,7 @@ OPENROUTER_API_KEY= # REQUIRED in production — generate with: openssl rand -hex 32 (or PowerShell random hex) # Without this, the app will refuse to start in production. VIEWS_IP_SALT= + +# --- Custom reverse (optional: agent service for deep / focus prompts) --- +# Run the `custom_reverse` service locally, then: +# CUSTOM_REVERSE_SERVICE_URL=http://localhost:3001 diff --git a/README.md b/README.md index 3d6ec82..dd74b79 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,14 @@ Next.js (App Router), React, TypeScript, Tailwind CSS, GitHub API, OpenRouter. Copy `.env.example` to `.env.local`. You need **`OPENROUTER_API_KEY`**. Optional: `OPENROUTER_MODEL` (defaults to `google/gemini-2.5-pro`), `GITHUB_TOKEN` for better GitHub rate limits, and Supabase env vars from the example file if you want server-side caching. +### Custom reverse (optional) + +For **deep / focus** prompts (clone + agent reads real files), run the **custom_reverse** TypeScript service locally (see that project’s `README`; `pnpm dev`, default port **3001**). In `.env.local` set: + +`CUSTOM_REVERSE_SERVICE_URL=http://localhost:3001` + +Then enable **Custom reverse** on the home page and describe what to reverse-engineer. + ## Development ```bash diff --git a/app/api/custom-reverse/route.ts b/app/api/custom-reverse/route.ts new file mode 100644 index 0000000..fe7f9e6 --- /dev/null +++ b/app/api/custom-reverse/route.ts @@ -0,0 +1,104 @@ +import { NextRequest, NextResponse } from "next/server"; + +const DEFAULT_CUSTOM_REVERSE_URL = "http://localhost:3001"; + +function getServiceUrl(): string { + return ( + process.env.CUSTOM_REVERSE_SERVICE_URL?.trim() || DEFAULT_CUSTOM_REVERSE_URL + ); +} + +/** Long-running agent; allow up to 10 minutes for slow clones + many tool turns. */ +const FETCH_TIMEOUT_MS = 600_000; + +export async function POST(request: NextRequest) { + let body: { repoUrl?: string; customPrompt?: string }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + + const repoUrl = body.repoUrl; + const customPrompt = body.customPrompt; + + if (typeof repoUrl !== "string" || !repoUrl.trim()) { + return NextResponse.json( + { error: "repoUrl is required (string)" }, + { status: 400 } + ); + } + if (typeof customPrompt !== "string" || !customPrompt.trim()) { + return NextResponse.json( + { error: "customPrompt is required (string)" }, + { status: 400 } + ); + } + + const base = getServiceUrl().replace(/\/$/, ""); + + let res: Response; + try { + const controller = new AbortController(); + const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + res = await fetch(`${base}/reverse`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ repoUrl: repoUrl.trim(), customPrompt: customPrompt.trim() }), + signal: controller.signal, + }); + } finally { + clearTimeout(t); + } + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + const isAbort = e instanceof Error && e.name === "AbortError"; + return NextResponse.json( + { + error: isAbort + ? "Custom reverse timed out. Try a smaller repo or a narrower prompt." + : `Custom reverse service unreachable (${msg}). Is custom-reverse running on ${base}?`, + }, + { status: 503 } + ); + } + + let data: unknown; + try { + data = await res.json(); + } catch { + return NextResponse.json( + { error: "Custom reverse service returned invalid JSON." }, + { status: 502 } + ); + } + + if (!res.ok) { + const err = + data && + typeof data === "object" && + "error" in data && + typeof (data as { error: unknown }).error === "string" + ? (data as { error: string }).error + : `Request failed (${res.status})`; + return NextResponse.json({ error: err }, { status: res.status >= 400 && res.status < 600 ? res.status : 502 }); + } + + const prompt = + data && + typeof data === "object" && + "prompt" in data && + typeof (data as { prompt: unknown }).prompt === "string" + ? (data as { prompt: string }).prompt + : null; + + if (!prompt) { + return NextResponse.json( + { error: "Custom reverse service did not return a prompt." }, + { status: 502 } + ); + } + + return NextResponse.json({ prompt }, { status: 200 }); +} diff --git a/components/reverse-prompt-home.tsx b/components/reverse-prompt-home.tsx index bdaaae2..f03e17d 100644 --- a/components/reverse-prompt-home.tsx +++ b/components/reverse-prompt-home.tsx @@ -37,6 +37,10 @@ export function ReversePromptHome({ repo, }: ReversePromptHomeProps) { const [repoUrl, setRepoUrl] = useState(initialRepoInput); + const [customReverse, setCustomReverse] = useState(false); + const [customPrompt, setCustomPrompt] = useState(""); + /** Keeps result card title accurate if user toggles mode after a run */ + const [lastResultWasCustom, setLastResultWasCustom] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [rateLimited, setRateLimited] = useState(false); @@ -71,6 +75,52 @@ export function ReversePromptHome({ } if (typeof data.prompt === "string") { setPrompt(data.prompt); + setLastResultWasCustom(false); + const parsed = parseGitHubRepoInput(input); + if (parsed && typeof window !== "undefined") { + window.history.replaceState( + null, + "", + `/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}` + ); + } + } else { + setError("No prompt in response."); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Request failed"); + } finally { + setLoading(false); + } + }, []); + + const runCustomReverse = useCallback(async (input: string, focus: string) => { + setError(null); + setRateLimited(false); + setPrompt(""); + setCopied(false); + setLoading(true); + try { + const res = await fetch("/api/custom-reverse", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ repoUrl: input, customPrompt: focus }), + }); + const data = (await res.json()) as { + prompt?: string; + error?: string; + }; + if (!res.ok) { + if (res.status === 429) { + setRateLimited(true); + return; + } + setError(data.error ?? `Request failed (${res.status})`); + return; + } + if (typeof data.prompt === "string") { + setPrompt(data.prompt); + setLastResultWasCustom(true); const parsed = parseGitHubRepoInput(input); if (parsed && typeof window !== "undefined") { window.history.replaceState( @@ -91,7 +141,12 @@ export function ReversePromptHome({ function onSubmit(e: React.FormEvent) { e.preventDefault(); - void runReversePrompt(repoUrl.trim()); + const trimmed = repoUrl.trim(); + if (customReverse) { + void runCustomReverse(trimmed, customPrompt.trim()); + } else { + void runReversePrompt(trimmed); + } } useEffect(() => { @@ -102,6 +157,8 @@ export function ReversePromptHome({ void runReversePrompt(trimmed); }, [autoSubmit, initialRepoInput, runReversePrompt]); + /* Custom reverse requires an explicit prompt — do not auto-submit on shared links. */ + useEffect(() => { if (typeof window === "undefined") return; const o = owner?.trim(); @@ -301,58 +358,87 @@ export function ReversePromptHome({ onSubmit={onSubmit} className="relative z-10 rounded-xl border-[3px] border-zinc-900 bg-[#fff4da] p-6" > -
-
-
- setRepoUrl(e.target.value)} - required - /> +
+
+
+
+ setRepoUrl(e.target.value)} + required + /> +
+
+
+ +
-
-
- +
+ + {customReverse ? ( +
+
+