Skip to content

Commit aac427a

Browse files
committed
Merge feat/leetcode-improvements: LeetCode overhaul + AI prompt centralization
2 parents 31bf822 + c4d0559 commit aac427a

14 files changed

Lines changed: 699 additions & 370 deletions

File tree

CLAUDE.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
---
6+
7+
## What CodeLedger Is
8+
9+
A **Manifest V3 browser extension** (Chrome + Firefox) that automatically commits solved DSA problems from LeetCode, GeeksForGeeks, and Codeforces to a user-owned GitHub repository. Backed by a **Cloudflare Worker** (Hono) that handles GitHub OAuth and serves the landing page.
10+
11+
- **Domain:** `codeledger.vkrishna04.me`
12+
- **Auth worker:** `https://codeledger.vkrishna04.me/api`
13+
- **Extension root:** `src/` — this is the directory loaded unpacked in Chrome
14+
- **Stack:** Pure ES6 modules, no bundler, no transpiler. Preact + htm from CDN. Tailwind CSS for the compiled stylesheet only.
15+
16+
---
17+
18+
## Commands
19+
20+
### Extension development
21+
```bash
22+
npm install
23+
npm run build:css # Tailwind → src/ui/styles/compiled.css (run after CSS changes)
24+
npm run build # CSS + dist packaging
25+
npm run watch # rebuild on file changes (dev mode)
26+
npm run lint # tsc --noEmit (type-check only, no transpile)
27+
```
28+
29+
Load the extension unpacked from `src/` in `chrome://extensions`.
30+
31+
### Worker (Cloudflare)
32+
```bash
33+
cd worker && npm install
34+
npx wrangler dev # local dev (requires wrangler.toml with secrets)
35+
npx wrangler deploy # deploy to production
36+
cd .. && npm run deploy:worker # shorthand from root
37+
```
38+
39+
`worker/wrangler.toml` is git-ignored — create it from the template in `CODELEDGER_EXECUTION_GUIDE.md`.
40+
41+
### Dev utilities
42+
```bash
43+
node dev/generate-manifest-domains.js # regenerates host_permissions from dom-selectors DOMAINS exports
44+
node dev/build-canonical-map.js # validate data/canonical-map.json against schema
45+
node dev/package-chrome.js # produce codeledger-chrome-vX.zip
46+
node dev/package-firefox.js # produce codeledger-firefox-vX.zip
47+
node dev/import-profile/leetcode-importer.js --github-token=TOKEN --repo=owner/repo
48+
node dev/import-profile/gfg-importer.js --github-token=TOKEN --repo=owner/repo
49+
```
50+
51+
### Smoke test (post-deploy)
52+
```bash
53+
curl -sf https://codeledger.vkrishna04.me/api/health
54+
```
55+
56+
---
57+
58+
## Architecture
59+
60+
### Extension layers (all in `src/`)
61+
62+
```
63+
manifest.json
64+
├── background/service-worker.js — SW: init, event bus, handles problem:solved
65+
│ ├── git-engine.js — atomic GitHub Tree API commits
66+
│ ├── sync-engine.js — cross-device sync via repo index.json
67+
│ └── alarm-manager.js — chrome.alarms for reminders/sync
68+
├── content/handler-loader.js — matches hostname → dynamically imports platform handler
69+
│ ├── heartbeat.js — SW keepalive port
70+
│ └── presence-marker.js — injects #codeledger-present on landing page
71+
├── handlers/
72+
│ ├── _base/BasePlatformHandler.js — safeQuery(), MutationObserver lifecycle
73+
│ ├── platforms/{leetcode,geeksforgeeks,codeforces}/index.js
74+
│ ├── ai/{gemini,openai,claude,deepseek,ollama}/index.js
75+
│ └── git/{github,gitlab,bitbucket}/index.js
76+
├── core/
77+
│ ├── constants.js — SINGLE SOURCE OF TRUTH for all URLs, keys, storage key names
78+
│ ├── storage.js — unified storage abstraction (wraps browser-compat)
79+
│ ├── event-bus.js — typed pub/sub (problem:solved → service-worker)
80+
│ ├── canonical-mapper.js — resolves platform problem → canonical ID
81+
│ └── ai-prompts.js — prompt templates + normalizeAIPrompts()
82+
├── lib/
83+
│ ├── browser-compat.js — THE ONLY FILE that uses chrome.* or browser.*
84+
│ └── debug.js — createDebugger() with console.bind() trick
85+
└── ui/components/SettingsSchema.js — schema-driven settings renderer (Preact + htm)
86+
```
87+
88+
### Data flow for a solve event
89+
1. Content script (`handler-loader.js`) → imports platform handler → calls `handler.init()`
90+
2. Platform handler detects accepted submission (DOM / GraphQL / REST)
91+
3. Fires `eventBus.emit("problem:solved", data)` → caught by service-worker
92+
4. SW saves to IndexedDB, optionally calls AI review, then calls `git-engine.js`
93+
5. `git-engine.js` calls GitHub Tree API for a single atomic commit
94+
95+
### Cloudflare Worker (`worker/src/index.js`)
96+
- Built with **Hono** framework
97+
- Routes: `/api/health`, `/api/auth/github`, `/api/auth/github/callback`, `/api/webhook/github`, `/api/admin/canonical`, `/api/data/canonical-map.json`
98+
- Serves static landing page from `worker/public/`
99+
- OAuth callback posts `{ type: 'CODELEDGER_AUTH', provider, token }` — the extension listens for exactly this message type
100+
101+
### Library / Web App (`src/library/`)
102+
- Shared HTML + Preact components used both inside the extension sidebar and at `codeledger.vkrishna04.me/library`
103+
- Auto-detects context: `IS_EXTENSION = !!chrome.runtime?.id`
104+
- Extension mode: reads IndexedDB; Web app mode: reads GitHub API via OAuth token
105+
106+
---
107+
108+
## Critical Rules
109+
110+
### Never use `chrome.*` or `browser.*` directly
111+
All extension API calls must go through `src/lib/browser-compat.js`. This is the only file that touches those namespaces.
112+
113+
### Never use `console.log` directly
114+
Use `createDebugger('HandlerName')` from `src/lib/debug.js`. The `.bind()` trick preserves caller file+line in DevTools.
115+
116+
```js
117+
import { createDebugger } from '../../lib/debug.js';
118+
const dbg = createDebugger('MyHandler');
119+
dbg.log('message'); // shows at the correct source location in DevTools
120+
```
121+
122+
### Import paths from extension pages
123+
The extension root is `src/`. `chrome.runtime.getURL('handlers/...')` — no `src/` prefix in the path. This is a common bug source.
124+
125+
### UI: Preact + htm, no build step
126+
All UI files import Preact and htm from `https://esm.sh`. No JSX. No webpack. No transpilation. Every UI file starts with:
127+
```js
128+
import { h, render } from '../../vendor/preact-bundle.js';
129+
import { useState, useEffect } from '../../vendor/preact-bundle.js';
130+
import { htm } from '../../vendor/preact-bundle.js';
131+
const html = htm.bind(h);
132+
```
133+
`src/vendor/preact-bundle.js` is a CDN re-export shim — all UI files import from this single path.
134+
135+
### OAuth message contract
136+
Worker posts: `{ type: 'CODELEDGER_AUTH', provider: 'github', token: '...' }`
137+
Extension listens for exactly `data.type === 'CODELEDGER_AUTH'`. Any mismatch silently drops the token.
138+
139+
### Token storage paths
140+
- OAuth tokens: `Storage.setAuthToken(provider, token)` → stored at `auth.tokens`
141+
- AI API keys: `Storage.setAIKeys(map)` → stored at `ai.keys`
142+
- Manual PAT: `settings['github_token']`
143+
- `GitHubHandler.getToken()` checks OAuth path first, then settings PAT — order matters.
144+
145+
---
146+
147+
## Current State (as of CODELEDGER_EXECUTION_GUIDE.md)
148+
149+
The execution guide defines 5 modules of fixes needed. Status of each:
150+
151+
| Module | Problem | Files |
152+
| ------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
153+
| M0 | vendor bundles need CDN re-export shims | `src/vendor/preact-bundle.js`, `src/vendor/chart-bundle.js` |
154+
| M1 | Worker: PKCS#1 key bug, wrong postMessage type, missing /api/health | `worker/src/index.js` |
155+
| M2 | `src/core/ai-prompts.js` does not exist → settings page crashes | CREATE `src/core/ai-prompts.js` |
156+
| M3 | handleOAuth saves to wrong storage; getToken() checks wrong order | `src/ui/components/SettingsSchema.js`, `src/handlers/git/github/index.js`, `src/background/service-worker.js` |
157+
| M4 | LeetCode: no debounce, double commits; manifest run_at wrong, WAR too narrow, handler-loader has extra `src/` prefix | `src/handlers/platforms/leetcode/index.js`, `src/manifest.json`, `src/content/handler-loader.js` |
158+
| M5 | `worker/public/config.json` has placeholder GitHub App slug | `worker/public/config.json` |
159+
160+
Apply modules in order M0 → M5. Each has a VERIFY section that must pass before proceeding.
161+
162+
---
163+
164+
## Worker Secrets (Wrangler)
165+
166+
| Secret name | Source |
167+
| ---------------------------------- | ------------------------------------------------------------ |
168+
| `CODELEDGER_GH_APP_PRIVATE_KEY` | PKCS#8 PEM file (convert PKCS#1 with `openssl pkcs8 -topk8`) |
169+
| `CODELEDGER_GH_APP_ID` | GitHub App numeric ID |
170+
| `CODELEDGER_GH_APP_CLIENT_ID` | GitHub App Client ID |
171+
| `CODELEDGER_GH_APP_CLIENT_SECRET` | GitHub App client secret |
172+
| `CODELEDGER_GH_APP_WEBHOOK_SECRET` | `openssl rand -hex 32` |
173+
| `CANONICAL_UPLOAD_TOKEN` | `openssl rand -hex 32` |
174+
| `SESSION_SECRET` | `openssl rand -hex 32` |
175+
176+
---
177+
178+
## Adding a New Platform Handler
179+
180+
1. Create `src/handlers/platforms/{name}/index.js` extending `BasePlatformHandler`
181+
2. Create `dom-selectors.js` with versioned `SELECTORS`, `LEGACY_SELECTORS`, and `DOMAINS` export
182+
3. Create `page-detector.js` with `detectPage()` and `isSolveCapablePage()`
183+
4. Add hostname match in `src/content/handler-loader.js`
184+
5. Run `node dev/generate-manifest-domains.js` to update `manifest.json` host_permissions
185+
6. See `docs/ADDING_PLATFORM_HANDLER.md` for full contract
186+
187+
## Adding a New AI Provider
188+
189+
1. Create `src/handlers/ai/{name}/index.js` extending `BaseAIHandler`
190+
2. Create `model-fetcher.js` that fetches live models (or static list for providers without a models endpoint)
191+
3. Add provider config to `CONSTANTS.AI_PROVIDERS` in `src/core/constants.js`
192+
4. Register settings schema in `src/handlers/init.js`
193+
5. Wire into `ModelSelector.js` `loadModels()` switch
194+
195+
---
196+
197+
## Branch Strategy
198+
199+
Use feature branches off `main`:
200+
- `fix/m0-vendor-bundles` — Module 0 vendor shims
201+
- `fix/m1-worker` — Worker OAuth + health endpoint
202+
- `fix/m2-ai-prompts` — Create ai-prompts.js
203+
- `fix/m3-oauth-wiring` — Token storage + getToken priority
204+
- `fix/m4-leetcode-dedup` — Debounce + manifest + handler-loader
205+
- `fix/m5-config` — Worker config.json slug
206+
207+
Merge each back to `main` after its VERIFY section passes.

src/core/storage.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { storage as browserStorage } from "../lib/browser-compat.js";
77
import { CONSTANTS } from "./constants.js";
88
import { createDebugger } from "../lib/debug.js";
9+
import { normalizeAIPrompts } from "./ai-prompts.js";
910
const dbg = createDebugger("Storage");
1011

1112
/**
@@ -61,6 +62,16 @@ export const Storage = {
6162
await browserStorage.local.set(payload);
6263
},
6364

65+
async getAIPrompts() {
66+
const res = await browserStorage.local.get(CONSTANTS.SK.AI_PROMPTS);
67+
return normalizeAIPrompts(res[CONSTANTS.SK.AI_PROMPTS] || {});
68+
},
69+
70+
async setAIPrompts(prompts) {
71+
const normalized = normalizeAIPrompts(prompts || {});
72+
await browserStorage.local.set({ [CONSTANTS.SK.AI_PROMPTS]: normalized });
73+
},
74+
6475
async getAuthToken(provider) {
6576
const keys = await browserStorage.local.get(CONSTANTS.SK.AUTH_TOKENS);
6677
const tokens = keys[CONSTANTS.SK.AUTH_TOKENS] || {};

src/core/url-state.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,22 @@ export function updateQueryParams(partial, options = {}) {
3636
}
3737
window.history.pushState(null, "", next);
3838
}
39+
40+
export function buildSettingsHref({
41+
tab = "settings",
42+
settingsTab,
43+
settingsSection,
44+
settingsProvider,
45+
settingsAdvanced,
46+
q,
47+
} = {}) {
48+
const params = new URLSearchParams();
49+
if (tab) params.set("tab", tab);
50+
if (settingsTab) params.set("settingsTab", settingsTab);
51+
if (settingsSection) params.set("settingsSection", settingsSection);
52+
if (settingsProvider) params.set("settingsProvider", settingsProvider);
53+
if (settingsAdvanced)
54+
params.set("settingsAdvanced", String(settingsAdvanced));
55+
if (q) params.set("q", q);
56+
return `${window.location.pathname}?${params.toString()}`;
57+
}

src/handlers/ai/claude/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BaseAIHandler } from "../../_base/BaseAIHandler.js";
77
import { APIKeyPool } from "../../../core/api-key-pool.js";
88
import { Storage } from "../../../core/storage.js";
99
import { CONSTANTS } from "../../../core/constants.js";
10+
import { buildReviewPrompt } from "../../../core/ai-prompts.js";
1011

1112
export class ClaudeHandler extends BaseAIHandler {
1213
constructor() {
@@ -26,7 +27,8 @@ export class ClaudeHandler extends BaseAIHandler {
2627
settings.aiEndpoint ||
2728
CONSTANTS.AI_PROVIDERS.claude.endpoint;
2829

29-
const prompt = `Review this DSA solution for "${problemContext.title}". Language: ${problemContext.language}, Difficulty: ${problemContext.difficulty}. Code: \`${code}\`. Provide Time/Space complexity, optimizations, and key patterns.`;
30+
const prompts = await Storage.getAIPrompts();
31+
const prompt = buildReviewPrompt(problemContext, code, prompts);
3032

3133
const keyCount = await this.keyPool.getKeyCount();
3234
if (!keyCount) throw new Error("No Claude API key available.");

src/handlers/ai/deepseek/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BaseAIHandler } from "../../_base/BaseAIHandler.js";
77
import { APIKeyPool } from "../../../core/api-key-pool.js";
88
import { Storage } from "../../../core/storage.js";
99
import { CONSTANTS } from "../../../core/constants.js";
10+
import { buildReviewPrompt } from "../../../core/ai-prompts.js";
1011

1112
export class DeepSeekHandler extends BaseAIHandler {
1213
constructor() {
@@ -22,7 +23,8 @@ export class DeepSeekHandler extends BaseAIHandler {
2223
settings.aiModel ||
2324
CONSTANTS.AI_PROVIDERS.deepseek.defaultModel;
2425

25-
const prompt = `Review this DSA solution for "${problemContext.title}". Language: ${problemContext.language}, Difficulty: ${problemContext.difficulty}. Code: \`${code}\`. Provide Time/Space complexity, optimizations, and key patterns.`;
26+
const prompts = await Storage.getAIPrompts();
27+
const prompt = buildReviewPrompt(problemContext, code, prompts);
2628

2729
const endpoint =
2830
settings.deepseek_endpoint ||

src/handlers/ai/gemini/index.js

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BaseAIHandler } from "../../_base/BaseAIHandler.js";
77
import { APIKeyPool } from "../../../core/api-key-pool.js";
88
import { Storage } from "../../../core/storage.js";
99
import { CONSTANTS } from "../../../core/constants.js";
10+
import { buildReviewPrompt } from "../../../core/ai-prompts.js";
1011

1112
export class GeminiHandler extends BaseAIHandler {
1213
constructor() {
@@ -55,22 +56,8 @@ export class GeminiHandler extends BaseAIHandler {
5556
settings.gemini_endpoint ||
5657
settings.aiEndpoint ||
5758
CONSTANTS.AI_PROVIDERS.gemini.endpoint;
58-
59-
const prompt = `
60-
Review the following DSA solution for the problem: "${problemContext.title}".
61-
Language: ${problemContext.language || problemContext.lang?.name || "Unknown"}
62-
Difficulty: ${problemContext.difficulty}
63-
64-
Code:
65-
\`\`\`
66-
${code}
67-
\`\`\`
68-
69-
Please provide a brief, professional analysis:
70-
1. Time & Space Complexity (using Big-O notation).
71-
2. Potential optimizations or cleaner approaches.
72-
3. Key take-away patterns.
73-
`;
59+
const prompts = await Storage.getAIPrompts();
60+
const prompt = buildReviewPrompt(problemContext, code, prompts);
7461

7562
const keyCount = await this.keyPool.getKeyCount();
7663
if (!keyCount) throw new Error("No Gemini API key available.");

src/handlers/ai/ollama/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { BaseAIHandler } from "../../_base/BaseAIHandler.js";
77
import { Storage } from "../../../core/storage.js";
88
import { CONSTANTS } from "../../../core/constants.js";
9+
import { buildReviewPrompt } from "../../../core/ai-prompts.js";
910

1011
export class OllamaHandler extends BaseAIHandler {
1112
constructor() {
@@ -56,7 +57,8 @@ export class OllamaHandler extends BaseAIHandler {
5657
const endpoint =
5758
settings.ollama_endpoint || CONSTANTS.AI_PROVIDERS.ollama.endpoint;
5859

59-
const prompt = `Review this DSA solution for "${problemContext.title}". Language: ${problemContext.language}, Difficulty: ${problemContext.difficulty}. Code: \`${code}\`. Provide Time/Space complexity, optimizations, and key patterns.`;
60+
const prompts = await Storage.getAIPrompts();
61+
const prompt = buildReviewPrompt(problemContext, code, prompts);
6062

6163
try {
6264
const res = await fetch(`${endpoint}/generate`, {

src/handlers/ai/openai/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BaseAIHandler } from "../../_base/BaseAIHandler.js";
77
import { APIKeyPool } from "../../../core/api-key-pool.js";
88
import { Storage } from "../../../core/storage.js";
99
import { CONSTANTS } from "../../../core/constants.js";
10+
import { buildReviewPrompt } from "../../../core/ai-prompts.js";
1011

1112
export class OpenAIHandler extends BaseAIHandler {
1213
constructor() {
@@ -26,7 +27,8 @@ export class OpenAIHandler extends BaseAIHandler {
2627
settings.aiEndpoint ||
2728
CONSTANTS.AI_PROVIDERS.openai.endpoint;
2829

29-
const prompt = `Review this DSA solution for "${problemContext.title}". Language: ${problemContext.language}, Difficulty: ${problemContext.difficulty}. Code: \`${code}\`. Provide Time/Space complexity, optimizations, and key patterns.`;
30+
const prompts = await Storage.getAIPrompts();
31+
const prompt = buildReviewPrompt(problemContext, code, prompts);
3032

3133
const keyCount = await this.keyPool.getKeyCount();
3234
if (!keyCount) throw new Error("No OpenAI API key available.");

src/handlers/ai/openrouter/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BaseAIHandler } from "../../_base/BaseAIHandler.js";
77
import { APIKeyPool } from "../../../core/api-key-pool.js";
88
import { Storage } from "../../../core/storage.js";
99
import { CONSTANTS } from "../../../core/constants.js";
10+
import { buildReviewPrompt } from "../../../core/ai-prompts.js";
1011

1112
export class OpenRouterHandler extends BaseAIHandler {
1213
constructor() {
@@ -26,7 +27,8 @@ export class OpenRouterHandler extends BaseAIHandler {
2627
settings.aiEndpoint ||
2728
CONSTANTS.AI_PROVIDERS.openrouter.endpoint;
2829

29-
const prompt = `Review this DSA solution for "${problemContext.title}". Language: ${problemContext.language}, Difficulty: ${problemContext.difficulty}. Code: \`${code}\`. Provide Time/Space complexity, optimizations, and key patterns.`;
30+
const prompts = await Storage.getAIPrompts();
31+
const prompt = buildReviewPrompt(problemContext, code, prompts);
3032

3133
const keyCount = await this.keyPool.getKeyCount();
3234
if (!keyCount) throw new Error("No OpenRouter API key available.");

0 commit comments

Comments
 (0)