CodeLedger is designed to be easily extensible. If you want to track solves from a new platform (e.g., HackerRank, AtCoder, CSES), you only need to create a new handler plugin.
The core architecture uses a unified eventBus to communicate between content scripts and background services. The background service handles all Git commits, AI reviews, and storage logic. Your platform handler simply needs to:
- Detect a successful solution.
- Extract the relevant metadata and code.
- Emit a standardized
problem:solvedevent.
Create a new folder in src/handlers/platforms/your-platform-name/. The typical structure is:
src/handlers/platforms/hackerrank/
├── index.js # Main handler class (extends BasePlatformHandler)
├── dom-selectors.js # CSS Selectors for the platform
├── page-detector.js # Logic to identify problem pages & slugs
Your handler must extend BasePlatformHandler and adhere to its core contract.
import { BasePlatformHandler } from "../../_base/BasePlatformHandler.js";
import { SELECTORS } from "./dom-selectors.js";
import { detectPage } from "./page-detector.js";
import { eventBus } from "../../../core/event-bus.js";
export class HackerRankHandler extends BasePlatformHandler {
constructor() {
super("hackerrank", "HackerRank", {});
this.mutationObserver = null;
this.lastDetectedId = null;
}
async init() {
this.dbg.log("Initializing HackerRank handler");
this.setupMutationObserver();
}
setupMutationObserver() {
// Observe the DOM for success messages
this.mutationObserver = new MutationObserver(() => {
this.checkSubmission();
});
this.mutationObserver.observe(document.body, {
childList: true,
subtree: true,
});
}
async checkSubmission() {
const successEl = this.safeQuery(SELECTORS.submission.successIndicator);
if (!successEl || !successEl.innerText.includes("Congratulations")) return;
const pageInfo = detectPage(window.location.pathname);
if (pageInfo.slug === this.lastDetectedId) return; // Prevent duplicate triggers
this.dbg.log("Solve detected!", pageInfo.slug);
this.lastDetectedId = pageInfo.slug;
// 1. Gather Code and Metadata
const code = this.safeQuery(SELECTORS.submission.code)?.innerText;
const title = this.safeQuery(SELECTORS.problem.title)?.innerText;
// 2. Emit Standardized Event payload
eventBus.emit("problem:solved", {
platform: "hackerrank",
id: pageInfo.slug, // Unique ID or SLUG
title: title, // Human readable title
titleSlug: pageInfo.slug, // URL slug
difficulty: "Medium", // 'Easy', 'Medium', or 'Hard'
topic: "Algorithms", // Primary topic category
tags: [], // Additional string tags
code: code, // The raw source code string
lang: {
name: "Python", // Human readable language
ext: "py", // File extension for GitHub
},
runtime: "N/A",
memory: "N/A",
timestamp: Date.now(),
});
}
}Once your module is created, register it so the extension loads it.
handler-registry.js holds the registry itself; init.js is what fills it.
Import the class and add an instance to the platforms array —
initializeHandlers() registers it and records its getSettingsSchema() if it
defines one (the schema is descriptive only; the settings UI is the hand-written
panels under src/library/settings-panels/).
// At the top of init.js, alongside the other platform imports
import { HackerRankHandler } from "./platforms/hackerrank/index.js";
// Inside initializeHandlers()
const platforms = [
new LeetCodeHandler(),
// …
new HackerRankHandler(),
];In src/content/handler-loader.js, add your platform to the domain router:
try {
if (hostname.includes('leetcode.com')) {
// ... existing leetcode loader
} else if (isHost('hackerrank.com', hostname)) {
console.log('[CodeLedger] Loading HackerRank handler...');
// NOTE: no `src/` prefix — the extension root IS `src/`, so a path that
// starts with `src/` resolves to nothing and the import fails silently.
const url = chrome.runtime.getURL('handlers/platforms/hackerrank/index.js');
const { HackerRankHandler } = await import(url);
const handler = new HackerRankHandler();
handler.init();
}
}Update both src/manifest-chromium.json and src/manifest-firefox.json to
allow content scripts to run on the platform and to whitelist it for dynamic
module resolution:
- Add domain to
host_permissions - Add domain to
content_scripts.matches - Add domain to
web_accessible_resources.matches - Add
handlers/platforms/your-platform/*toweb_accessible_resources.resources
In practice you export DOMAINS from the handler's dom-selectors.js, add the
domain to PLATFORM_DOMAINS in dev/generate-manifest-domains.js, and run it —
it writes the first three into both manifests for you. The resources list in
step 4 is not generated; add that line by hand, or the dynamic
import() in the loader is blocked at runtime.
"host_permissions": [
"*://*.leetcode.com/*",
"*://*.hackerrank.com/*"
]Some platforms are single-page apps that judge a submission over fetch and
never render a durable "Accepted" node — NeetCode and takeuforward both behave
this way. For those, src/content/net-tap.js runs in the page's MAIN world
("world": "MAIN" on its own content_scripts entry) and mirrors matching
requests back over window.postMessage;
src/lib/net-tap-client.js validates and delivers them to the handler.
Reach for it only when the DOM genuinely cannot answer the question, and when you do:
- Add your endpoint to the allow-list in
net-tap.js. It mirrors nothing else. - Add your host to
TAP_DOMAINSindev/generate-manifest-domains.js. The tap must stay scoped to the hosts that need it, never site-wide. - Treat every tapped message as a claim, not a fact. The page shares that
world and can post whatever it likes, so the detector has to satisfy itself
that the payload describes a real accepted submission. See the header comment
in
net-tap-client.jsfor the exact boundary and the residual risk.
world: "MAIN" needs Chrome 111+ and Firefox 128+; the Firefox manifest already
declares strict_min_version: "142.0", so both targets support it.
Ensure your handler emits exactly this shape when calling eventBus.emit('problem:solved', payload):
platform(string): Lowercase provider ID ('leetcode', 'hackerrank').id(string|number): Unique ID of the problem.title(string): Title of problem.titleSlug(string): URL-friendly string.difficulty(string):Easy,Medium, orHard.topic(string): Broad category folder name (e.g.Arrays,Dynamic Programming).tags(string[]): Descriptive tags.code(string): The actual solution.lang(object):name: E.g., 'C++', 'Python3', 'Java'.ext: E.g., 'cpp', 'py', 'java' (Used for git file extension).
timestamp(number): Unix time in milliseconds (Date.now()), not seconds.