Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
378f9d6
chore(views): require VIEWS_IP_SALT in production, document in .env.e…
filiksyos Apr 12, 2026
dfc40b1
Merge pull request #35 from filiksyos/simpler-homepage
filiksyos Apr 12, 2026
3c70ed4
whatever
filiksyos Apr 13, 2026
e7b6d42
Merge pull request #36 from filiksyos/home-example-repo-exclusion
filiksyos Apr 13, 2026
88b8107
removed cache ttl
filiksyos Apr 13, 2026
d9ce7d3
Merge pull request #37 from filiksyos/no-cache-ttl
filiksyos Apr 13, 2026
ea0c8c0
whatever
filiksyos Apr 13, 2026
c4b8f19
Merge pull request #38 from filiksyos/smple
filiksyos Apr 13, 2026
ca1a783
custom reverse
filiksyos Apr 14, 2026
acfccb2
custom reverse beta
filiksyos Apr 15, 2026
110e75d
Merge pull request #39 from filiksyos/beta-invite-only
filiksyos Apr 15, 2026
598e8a9
redirect for now
filiksyos Apr 20, 2026
e1a5477
Merge pull request #40 from filiksyos/subfolder-redirect
filiksyos Apr 20, 2026
08357c6
customr reverse
filiksyos Apr 21, 2026
19e9c8b
Merge pull request #41 from filiksyos/custom-reverse-v1
filiksyos Apr 21, 2026
3631f09
renamed to manual control
filiksyos Apr 22, 2026
d83f186
Merge pull request #42 from filiksyos/manual-control
filiksyos Apr 22, 2026
9506c9a
..
filiksyos Apr 22, 2026
5e07b1b
Merge pull request #43 from filiksyos/manual-control
filiksyos Apr 22, 2026
dddf6b3
whatever
filiksyos Apr 24, 2026
d2694e7
reverse engineered prompt
filiksyos Apr 24, 2026
590fd35
Merge pull request #44 from filiksyos/deep-reverse
filiksyos Apr 24, 2026
5d20037
prompt caching for deep reverse
filiksyos Apr 24, 2026
0d14c10
Merge pull request #45 from filiksyos/deep-caching
filiksyos Apr 24, 2026
3a6acf1
sse progress
filiksyos Apr 24, 2026
bc449db
build fix
filiksyos Apr 24, 2026
c07a162
Merge pull request #46 from filiksyos/progress-message
filiksyos Apr 24, 2026
3f2d408
separate urls for deep and manual + history improvement
filiksyos Apr 25, 2026
c4d97e2
refactor history handling in HistoryPage and remove unused localStora…
filiksyos Apr 25, 2026
6b512ee
Merge pull request #47 from filiksyos/history,-deep-and-manual-urls
filiksyos Apr 25, 2026
d507b73
fix: open manual control panel when loading focus URLs from history
cursoragent Apr 26, 2026
2f0d343
wtvr
filiksyos Apr 27, 2026
48e5c3b
Merge pull request #49 from filiksyos/cursor/manual-control-link-ui-7b80
filiksyos Apr 27, 2026
ca01679
rate limit deep reverse and manual control
filiksyos Apr 27, 2026
f1556e8
Merge pull request #50 from filiksyos/Rate-limiting
filiksyos Apr 27, 2026
7d079f1
stripe buy link integration
filiksyos Apr 27, 2026
bff2982
Merge pull request #51 from filiksyos/buy-button
filiksyos Apr 27, 2026
32cabe1
the url is the state
filiksyos Apr 27, 2026
56859a3
Merge pull request #52 from filiksyos/url-as-state
filiksyos Apr 27, 2026
6a63f76
clean relief
filiksyos Apr 27, 2026
ac61a71
wtvr
filiksyos Apr 27, 2026
1f0478b
wtvr
filiksyos Apr 27, 2026
9fba03e
Merge pull request #53 from filiksyos/cleaner-owner/repo
filiksyos Apr 27, 2026
d8132e6
whatever
filiksyos Apr 28, 2026
290fec4
build fix
filiksyos Apr 28, 2026
52a8893
Merge pull request #54 from filiksyos/grok
filiksyos Apr 28, 2026
5b69f85
markdown support
filiksyos Apr 29, 2026
8e0b020
Merge pull request #55 from filiksyos/markdown-support-for-prompts
filiksyos Apr 29, 2026
c02e031
checkout
filiksyos Apr 30, 2026
36e6532
Merge pull request #56 from filiksyos/stripe-checkout
filiksyos Apr 30, 2026
2a5fa11
url update on get prompt clicked
filiksyos Apr 30, 2026
c35dfe0
Merge pull request #57 from filiksyos/pushstate
filiksyos Apr 30, 2026
25ec9df
whatever
filiksyos May 1, 2026
4afdde4
Merge pull request #58 from filiksyos/checkout-marked
filiksyos May 1, 2026
f7ff8d4
checkout
filiksyos May 3, 2026
5e549a2
Merge pull request #59 from filiksyos/checkout-abandonment-form
filiksyos May 3, 2026
2b71df5
steal
filiksyos May 5, 2026
f32e2fa
Merge pull request #60 from filiksyos/steal-any-code-and-make-it-your…
filiksyos May 5, 2026
ac959fc
update
filiksyos May 5, 2026
68bb6ee
Merge pull request #61 from filiksyos/change-readme
filiksyos May 5, 2026
f4c9a80
geist
filiksyos May 5, 2026
8b2bba8
Merge pull request #62 from filiksyos/better-geist
filiksyos May 5, 2026
4bd514a
popup oauth
filiksyos May 6, 2026
ddd90b5
improved design
filiksyos May 6, 2026
c31f635
premium page
filiksyos May 6, 2026
b0913d1
premium page
filiksyos May 6, 2026
6ae0eef
navigate
filiksyos May 6, 2026
6b88e27
Merge pull request #63 from filiksyos/premium
filiksyos May 6, 2026
f3f924e
monthly limit
filiksyos May 6, 2026
376a896
xx
filiksyos May 6, 2026
0365775
Merge pull request #64 from filiksyos/server-side-limiting
filiksyos May 6, 2026
38425d6
limit to 1
filiksyos May 7, 2026
165fdf1
Merge pull request #65 from filiksyos/limits-to-1
filiksyos May 7, 2026
36ff77a
Update Discord invite link
cursoragent May 9, 2026
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
27 changes: 23 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,39 @@ SUPABASE_PUBLISHABLE_KEY=
# --- GitHub (optional: unauthenticated requests have lower API rate limits) ---
GITHUB_TOKEN=

# --- LLM: configure OpenRouter **or** Google AI Studio (OpenRouter is checked first) ---
# --- Quick LLM (GET prompt / reverse-prompt API) ---
# Pick the provider in one line. Keys for other providers can stay in the file; only the selected one is used.
# GITREVERSE_QUICK_LLM=auto
# auto — first key wins: Grok → OpenRouter → OpenAI → Google (same as leaving this unset)
# grok | openrouter | openai | google — require that provider’s API key only

# XAI_API_KEY=
# XAI_MODEL=grok-3 # or e.g. grok-4.20-0309-non-reasoning

OPENROUTER_API_KEY=
# OPENROUTER_MODEL=google/gemini-2.5-pro

# OPENAI_API_KEY=
# OPENAI_MODEL=gpt-4.1 # or e.g. gpt-5.5

# GOOGLE_GENERATIVE_AI_API_KEY=
# GOOGLE_AI_STUDIO_MODEL=gemini-2.5-pro

# Optional OpenRouter attribution headers (recommended for rankings)
# 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=

# --- Custom reverse (optional) ---
# Backend base URL for Custom reverse (local or your own deployment).
# CUSTOM_REVERSE_SERVICE_URL=http://localhost:3001

# --- Stripe checkout (optional: “Get Unlimited” on rate limit) ---
# Server-side Checkout Session — success URL is set in code (?session_id={CHECKOUT_SESSION_ID}).
# STRIPE_SECRET_KEY=sk_live_... # or sk_test_... for test mode
# STRIPE_PRICE_ID=price_1TQj8FIBG5KwEK8atVJ49Oq9 # GitReverse $9/mo (or your price id)
# Fallback if /api/create-checkout fails (optional):
# NEXT_PUBLIC_STRIPE_PAYMENT_LINK=https://buy.stripe.com/...
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
debug.log

# env files (can opt-in for committing if needed)
.env*
Expand Down
51 changes: 45 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,58 @@ https://github.com/user-attachments/assets/f0cdb7b2-c6f0-4483-8a01-153170479f2e

Turn a **public GitHub repository** into a **single synthetic user prompt** that someone might paste into Cursor, Claude Code, Codex, etc. to vibe code the project from scratch.

The app pulls **repo metadata**, a **root file tree** (depth 1), and the **README**, then uses an LLM via [OpenRouter](https://openrouter.ai/) to produce one short, conversational prompt grounded in that context.
The app pulls **repo metadata**, a **root file tree** (depth 1), and the **README**, then uses an LLM to produce one short, conversational prompt grounded in that context.

Paste a GitHub URL or `owner/repo` on the home page. You can also open **`/owner/repo`** (e.g. `/vercel/next.js`) for a shareable link that runs the same flow.

GitHub-style **`/owner/repo/tree/...`** URLs on this site **redirect to `/owner/repo`** so they do not 404. The reverse flow still uses the whole repo for now; **subfolder-aware** context (scoped to that path) is planned for a later change.

## Stack

Next.js (App Router), React, TypeScript, Tailwind CSS, GitHub API, OpenRouter.
Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 4, GitHub API, Supabase (optional), Stripe (optional).

## Configuration

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.
Copy `.env.example` to `.env.local` and fill in at least one LLM API key.

### Quick LLM (required)

The quick reverse endpoint supports four providers. Set **`GITREVERSE_QUICK_LLM`** to pin one, or leave it unset (`auto`) to let the app use whichever key it finds first:

| Provider | Key env var | Model env var | Default model |
|---|---|---|---|
| Grok (xAI) | `XAI_API_KEY` | `XAI_MODEL` | `grok-3` |
| OpenRouter | `OPENROUTER_API_KEY` | `OPENROUTER_MODEL` | `google/gemini-2.5-pro` |
| OpenAI | `OPENAI_API_KEY` | `OPENAI_MODEL` | `gpt-4.1` |
| Google AI Studio | `GOOGLE_GENERATIVE_AI_API_KEY` | `GOOGLE_AI_STUDIO_MODEL` | `gemini-2.5-pro` |

In `auto` mode the order of preference is: Grok → OpenRouter → OpenAI → Google.

### Other env vars

- **`GITHUB_TOKEN`** — optional; increases GitHub API rate limits.
- **`SUPABASE_URL`** + **`SUPABASE_PUBLISHABLE_KEY`** — optional; enables server-side caching of quick prompts in `prompt_cache` and exposes the `/library` page.
- **`VIEWS_IP_SALT`** — **required in production**. Generate one with `openssl rand -hex 32`. The app will refuse to start in production without a non-default value.

### Custom reverse (optional)

For **deep / focus** prompts, point the app at a backend service:

```
CUSTOM_REVERSE_SERVICE_URL=http://localhost:3001
```

## Routes

| Path | Description |
|---|---|
| `/` | Home — quick and custom reverse |
| `/library` | Browse cached quick prompts (requires Supabase) |
| `/history` | Recent repos from localStorage |
| `/[owner]/[repo]` | Shareable quick-reverse link |
| `/[owner]/[repo]/deep` | Shareable deep-reverse link |
| `/[owner]/[repo]/[focus]` | Shareable manual-focus link |
| `/[owner]/[repo]/tree/...` | Redirects to `/[owner]/[repo]` |

## Development

Expand All @@ -29,6 +70,4 @@ Open [http://localhost:3000](http://localhost:3000).
pnpm build
pnpm start
pnpm lint
```

Shout out to [GitIngest](http://github.com/coderamp-labs/gitingest) for inspiration.
```
69 changes: 69 additions & 0 deletions app/[owner]/[repo]/[focus]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { notFound } from "next/navigation";
import { connection } from "next/server";
import { ReversePromptHome } from "@/components/reverse-prompt-home";
import { focusFingerprint } from "@/lib/focus-fingerprint";
import { isValidGitHubRepoPath, normalizeRepoSegment } from "@/lib/parse-github-repo";
import { getSupabase } from "@/lib/supabase";

type PageProps = {
params: Promise<{ owner: string; repo: string; focus: string }>;
};

export default async function RepoFocusPage({ params }: PageProps) {
await connection();
const { owner: ownerRaw, repo: repoRaw, focus: focusRaw } = await params;
const owner = decodeURIComponent(ownerRaw);
const repo = decodeURIComponent(repoRaw);
let focus: string;
try {
focus = decodeURIComponent(focusRaw);
} catch {
notFound();
}

if (!isValidGitHubRepoPath(owner, repo)) {
notFound();
}

const trimmedFocus = focus.trim();
if (!trimmedFocus) {
notFound();
}

const repoNorm = normalizeRepoSegment(repo);
const initialRepoInput = `${owner}/${repoNorm}`;
const fp = focusFingerprint(trimmedFocus);

let cachedPrompt: string | undefined;
try {
const supabase = getSupabase();
if (supabase) {
const { data } = await supabase
.from("custom_prompt_cache")
.select("prompt")
.eq("owner", owner)
.eq("repo", repoNorm)
.eq("focus_fingerprint", fp)
.maybeSingle();
if (data?.prompt) {
cachedPrompt = data.prompt as string;
}
}
} catch {
// fall back to client auto-submit
}

return (
<ReversePromptHome
initialRepoInput={initialRepoInput}
autoSubmit={false}
autoSubmitFocus={cachedPrompt ? undefined : trimmedFocus}
initialPrompt={cachedPrompt}
owner={owner}
repo={repoNorm}
preserveUrl
initialGenerationKind={cachedPrompt ? "manual" : undefined}
initialManualFocus={cachedPrompt ? trimmedFocus : undefined}
/>
);
}
57 changes: 57 additions & 0 deletions app/[owner]/[repo]/deep/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { notFound } from "next/navigation";
import { connection } from "next/server";
import { ReversePromptHome } from "@/components/reverse-prompt-home";
import { DEEP_REVERSE_FOCUS, focusFingerprint } from "@/lib/focus-fingerprint";
import { isValidGitHubRepoPath, normalizeRepoSegment } from "@/lib/parse-github-repo";
import { getSupabase } from "@/lib/supabase";

type PageProps = {
params: Promise<{ owner: string; repo: string }>;
};

export default async function RepoDeepPage({ params }: PageProps) {
await connection();
const { owner: ownerRaw, repo: repoRaw } = await params;
const owner = decodeURIComponent(ownerRaw);
const repo = decodeURIComponent(repoRaw);

if (!isValidGitHubRepoPath(owner, repo)) {
notFound();
}

const repoNorm = normalizeRepoSegment(repo);
const initialRepoInput = `${owner}/${repoNorm}`;
const fp = focusFingerprint(DEEP_REVERSE_FOCUS);

let cachedPrompt: string | undefined;
try {
const supabase = getSupabase();
if (supabase) {
const { data } = await supabase
.from("custom_prompt_cache")
.select("prompt")
.eq("owner", owner)
.eq("repo", repoNorm)
.eq("focus_fingerprint", fp)
.maybeSingle();
if (data?.prompt) {
cachedPrompt = data.prompt as string;
}
}
} catch {
// fall back to client auto-submit
}

return (
<ReversePromptHome
initialRepoInput={initialRepoInput}
autoSubmit={false}
autoSubmitDeep={!cachedPrompt}
initialPrompt={cachedPrompt}
owner={owner}
repo={repoNorm}
preserveUrl
initialGenerationKind={cachedPrompt ? "deep" : undefined}
/>
);
}
1 change: 1 addition & 0 deletions app/[owner]/[repo]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default async function RepoPage({ params }: PageProps) {
initialPrompt={cachedPrompt}
owner={owner}
repo={repoNorm}
initialGenerationKind={cachedPrompt ? "quick" : undefined}
/>
);
}
25 changes: 25 additions & 0 deletions app/[owner]/[repo]/tree/[[...path]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { notFound, redirect } from "next/navigation";
import { connection } from "next/server";
import { isValidGitHubRepoPath, normalizeRepoSegment } from "@/lib/parse-github-repo";

type PageProps = {
params: Promise<{ owner: string; repo: string; path?: string[] }>;
};

/**
* GitHub-style `/owner/repo/tree/branch/...` → `/owner/repo` (avoids 404).
* Subfolder-scoped reverse context: planned for later; see README.
*/
export default async function RepoTreeRedirectPage({ params }: PageProps) {
await connection();
const { owner: ownerRaw, repo: repoRaw } = await params;
const owner = decodeURIComponent(ownerRaw);
const repo = decodeURIComponent(repoRaw);

if (!isValidGitHubRepoPath(owner, repo)) {
notFound();
}

const repoNorm = normalizeRepoSegment(repo);
redirect(`/${encodeURIComponent(owner)}/${encodeURIComponent(repoNorm)}`);
}
23 changes: 23 additions & 0 deletions app/api/check-subscription/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { checkActiveSubscriber } from "@/lib/subscriber";

export const runtime = "nodejs";

const MAX_EMAIL_LEN = 320;

export async function GET(req: NextRequest) {
const email = req.nextUrl.searchParams.get("email")?.trim();
if (!email || email.length > MAX_EMAIL_LEN) {
return NextResponse.json({ error: "bad_email" }, { status: 400 });
}

const subscribed = await checkActiveSubscriber(email);
if (subscribed === null) {
return NextResponse.json(
{ subscribed: false, degraded: true },
{ status: 200 }
);
}

return NextResponse.json({ subscribed });
}
55 changes: 55 additions & 0 deletions app/api/checkout-abandonment/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NextRequest, NextResponse } from "next/server";
import { getSupabase } from "@/lib/supabase";

export const runtime = "nodejs";

const ALLOWED_REASONS = new Set([
"too_expensive",
"not_ready_yet",
"just_browsing",
"other",
]);

export async function POST(req: NextRequest) {
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON." }, { status: 400 });
}

if (typeof body !== "object" || body === null || !("reason" in body)) {
return NextResponse.json(
{ error: "Expected JSON body with reason." },
{ status: 400 }
);
}

const reason = (body as { reason: unknown }).reason;
if (typeof reason !== "string" || !ALLOWED_REASONS.has(reason)) {
return NextResponse.json({ error: "Invalid reason." }, { status: 400 });
}

const rawOtherText = (body as { other_text?: unknown }).other_text;
const otherText =
reason === "other" && typeof rawOtherText === "string"
? rawOtherText.trim().slice(0, 1000)
: null;

const supabase = getSupabase();
if (!supabase) {
return NextResponse.json({ ok: true });
}

const { error } = await supabase.from("checkout_abandonment_responses").insert({
reason,
...(otherText ? { other_text: otherText } : {}),
});

if (error) {
console.warn("[checkout-abandonment]", error.message);
return NextResponse.json({ error: error.message }, { status: 500 });
}

return NextResponse.json({ ok: true });
}
Loading