Skip to content
Open
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
output
.idea
.vscode
__pycache__
*.raw
*.local.*
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ This project contains a comprehensive knowledge base about LiveKit, a real-time
- **Format**: Plain text format optimized for language models
- **Update script**: `python sync_livekit_repos.py` (included in repo sync)

### 5. **CRITICAL: LLM Guidance** (`knowledge_guidance.md`)
### 5. Chrome Extension (`composite_chrome_helper/` directory)
- **Content**: Chrome extension that adds a "Go To Source" button to navigate from composite repo URLs to their original source repositories
- **Branch data**: `composite_chrome_helper/repo-default-branches.json` - mapping of repo names to default branches
- **Update script**: `python update_chrome_helper_branches.py`
- **Purpose**: Fetches current default branch info from GitHub API for accurate source repo navigation

### 6. **CRITICAL: LLM Guidance** (`knowledge_guidance.md`)
- **Source**: Curated clarifications and corrections for common misunderstandings
- **Purpose**: Prevents misleading or incorrect responses about LiveKit
- **Content**: Specific guidance on issues that have caused confusion in the past
Expand Down
58 changes: 58 additions & 0 deletions composite_chrome_helper/404-detector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Content script for 404 detection and redirect handling
* All business logic is in the background service worker
*/

const browser = typeof globalThis.chrome !== 'undefined' ? globalThis.chrome : globalThis.browser;

/**
* Check if current page is a 404 on a LiveKit repo
* @returns {boolean}
*/
function isLiveKitRepo404() {
// Check if it's a 404 page
const is404 = document.title.includes('Page not found') ||
document.title.includes('404') ||
document.querySelector('img[alt="404"]') !== null;

if (!is404) return false;

// Check if it's a livekit or livekit-examples repo
const url = window.location.href;
return url.startsWith('https://github.com/livekit/') ||
url.startsWith('https://github.com/livekit-examples/');
}

/**
* Handle 404 detection and request redirect from background worker
*/
async function handle404Redirect() {
if (!isLiveKitRepo404()) {
return;
}

console.log('[LiveKit Extension] 404 detected, requesting redirect check from background...');

try {
const response = await browser.runtime.sendMessage({
type: 'handleLiveKit404',
url: window.location.href
});

if (response.redirectUrl) {
console.log(`[LiveKit Extension] Redirecting to: ${response.redirectUrl}`);
window.location.href = response.redirectUrl;
}
} catch (error) {
console.error('[LiveKit Extension] Failed to handle 404:', error);
}
}

// Run 404 handler when page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(handle404Redirect, 500);
});
} else {
setTimeout(handle404Redirect, 500);
}
12 changes: 12 additions & 0 deletions composite_chrome_helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,16 @@ While in a `https://github.com/livekit/livekit_composite/...` repository click t

## How to develop:

1. Make changes to the source files
2. Go to `chrome://extensions` and click the refresh icon on the extension
3. Test your changes

## Future Improvements

- [ ] Add a bundler (esbuild/rollup) to enable shared code between content scripts and background, TypeScript support, and minification

## Security Considerations

- [ ] **Remove empty `content-script.js`** - currently injects into all URLs with `MAIN` world context, which allows page JS access on every site. Should be removed or scoped to specific domains.
- [ ] **Remove unused `debugger` permission** - allows full DevTools Protocol access (pause execution, intercept network, capture screenshots). Not currently used.
- [ ] **Remove or fix `fillField()` function** (main.js) - not currently used, and uses `innerHTML` which could allow XSS. If needed in future, use `textContent` or `createElement` instead.
5 changes: 5 additions & 0 deletions composite_chrome_helper/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Background service worker entry point
// Import all background modules here
// Manifest V3 only allows one service worker entry point

import './lk-gh-resolver-bg.js';
215 changes: 215 additions & 0 deletions composite_chrome_helper/lk-gh-resolver-bg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/**
* LiveKit GitHub URL Resolver - Background Service Worker
*
* ASSUMPTION: All callers (main.js, 404-detector.js) validate that URLs are
* github.com/livekit/* or github.com/livekit-examples/* before calling these functions.
* This file does NOT perform that sanity check - it trusts the upstream validation.
*/

// In-memory branch cache for all tabs to share
let branchCache = null;

/**
* Prefetch and cache the branch data from JSON file
* Only runs once when background service worker starts
*/
async function prefetchCache() {
if (!branchCache) {
console.log('[LiveKit Extension] Prefetching branch cache in background...');
try {
const res = await fetch(chrome.runtime.getURL('repo-default-branches.json'));
branchCache = await res.json();
console.log('[LiveKit Extension] Branch cache prefetched and ready');
} catch (err) {
console.warn('[LiveKit Extension] Failed to load branch cache, using empty cache:', err);
branchCache = {};
}
}
return branchCache;
}

/**
* Get the default branch for a repo
* @param {string} org - Organization name (e.g., "livekit", "livekit-examples")
* @param {string} repo - Repository name (e.g., "agents")
* @returns {string} - Default branch name (e.g., "main", "master")
*/
function getDefaultBranch(org, repo) {
const entry = branchCache?.[org]?.[repo];
return entry?.branch || 'main';
}

/**
* Check if a repo is marked as an override in the cache
* @param {string} org - Organization name
* @param {string} repo - Repository name
* @returns {boolean} - True if marked as override
*/
function isOverride(org, repo) {
const entry = branchCache?.[org]?.[repo];
return entry?.isOverride === true;
}

/**
* Update the branch cache for a specific repo
* @param {string} org - Organization name
* @param {string} repo - Repository name
* @param {string} branch - Default branch name
*/
function updateBranchCache(org, repo, branch) {
if (!branchCache[org]) {
branchCache[org] = {};
}
branchCache[org][repo] = { branch: branch };
console.log(`[LiveKit Extension] Cache updated: ${org}/${repo} -> ${branch}`);
}

/**
* Fetch default branch from GitHub API
* @param {string} org - Organization name
* @param {string} repo - Repository name
* @returns {Promise<string>} - Default branch name
*/
async function fetchDefaultBranchFromAPI(org, repo) {
try {
const response = await fetch(`https://api.github.com/repos/${org}/${repo}`);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const data = await response.json();
return data.default_branch;
} catch (error) {
console.error(`[LiveKit Extension] Failed to fetch branch for ${org}/${repo}:`, error);
return 'main';
}
}

/**
* Converts a LiveKit composite repository URL to its source repository URL
*
* NOTE: This parses COMPOSITE URLs where the source org/repo are NESTED in the path.
* Do not merge with parseSourceRepoUrl() - they handle different URL structures.
*
* Input: https://github.com/livekit/livekit_composite/blob/{branch}/{org}/{repo}/{filePath}
* Output: https://github.com/{org}/{repo}/blob/{branch}/{filePath}
*
* @param {string} url - The composite URL to convert
* @returns {string|null} - The converted source URL, or null if conversion fails
*/
function convertCompositeToSource(url) {
const parts = new URL(url).pathname.split('/').filter(Boolean);
// parts: [livekit, livekit_composite, type, branch, org, repo, ...filePath]
// 0 1 2 3 4 5 6+
if (parts[2] !== 'blob') {
return null; // TODO: handle 'tree' in the future
}

if (parts.length < 7) {
return null;
}

const [, , , , org, repo, ...filePathParts] = parts;
const filePath = filePathParts.join('/');
const branch = getDefaultBranch(org, repo);
return `https://github.com/${org}/${repo}/blob/${branch}/${filePath}?utm_source=livekit_extension`;
}

/**
* Parse a source repo URL (after redirect from composite) to extract org, repo, and file path
*
* NOTE: This parses SOURCE REPO URLs where org/repo are at the ROOT of the path.
* Do not merge with convertCompositeToSource() - they handle different URL structures.
*
* Input: https://github.com/{org}/{repo}/blob/{branch}/{filePath}
* Used by: tryResolveStaleBranch404() to re-resolve 404s from stale branch cache
*
* @param {string} url - GitHub source repo URL
* @returns {object|null} - { org, repo, filePath } or null
*/
function parseSourceRepoUrl(url) {
const parts = new URL(url).pathname.split('/').filter(Boolean);
// parts: [org, repo, 'blob', branch, ...filePath]
// 0 1 2 3 4+
if (parts.length < 5 || parts[2] !== 'blob') {
return null;
}

const [org, repo, , , ...filePathParts] = parts;
return { org, repo, filePath: filePathParts.join('/') };
}

/**
* Try to resolve a 404 caused by stale branch cache
* @param {string} url - Current page URL
* @returns {Promise<string|null>} - Redirect URL or null
*/
async function tryResolveStaleBranch404(url) {
// Only handle redirects that came from our extension
if (!url.includes('utm_source=livekit_extension')) {
console.log('[LiveKit Extension] 404 not from extension, ignoring');
return null;
}

const parsed = parseSourceRepoUrl(url);
if (!parsed) {
return null;
}

const { org, repo, filePath } = parsed;

// If this is an override repo, trust the JSON and don't check API
if (isOverride(org, repo)) {
console.log(`[LiveKit Extension] Override repo ${org}/${repo}, file not found`);
return null;
}

console.log(`[LiveKit Extension] 404 detected for ${org}/${repo}, checking GitHub API...`);

// Get cached branch and fetch actual branch from API
const cachedBranch = getDefaultBranch(org, repo);
const apiBranch = await fetchDefaultBranchFromAPI(org, repo);

// If they match, the cache is correct - this is a real 404
if (cachedBranch === apiBranch) {
console.log(`[LiveKit Extension] Branch is correct (${cachedBranch}), file not found`);
return null;
}

// Cache is stale, update it
console.log(`[LiveKit Extension] Cache stale: ${cachedBranch} -> ${apiBranch}, updating and redirecting`);
updateBranchCache(org, repo, apiBranch);

// Return the correct URL
return `https://github.com/${org}/${repo}/blob/${apiBranch}/${filePath}?utm_source=livekit_extension`;
}

/**
* Handle messages from content scripts and popup
*/
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'convertCompositeUrl') {
// Convert composite URL to source URL (used by popup)
prefetchCache().then(() => {
const result = convertCompositeToSource(msg.url);
sendResponse({ url: result });
}).catch(err => {
console.error('[LiveKit Extension] Error converting URL:', err);
sendResponse({ url: null });
});
return true;
}

if (msg.type === 'handleLiveKit404') {
// Handle 404 redirect logic (used by content script)
prefetchCache().then(async () => {
const redirectUrl = await tryResolveStaleBranch404(msg.url);
sendResponse({ redirectUrl });
}).catch(err => {
console.error('[LiveKit Extension] Error handling 404:', err);
sendResponse({ redirectUrl: null });
});
return true;
}
});

console.log('[LiveKit Extension] Background service worker initialized');
36 changes: 23 additions & 13 deletions composite_chrome_helper/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ function fillField(container, name, value, tabId) {
container.appendChild(entry);
}

if (typeof browser === "undefined") {
var browser = chrome;
}
const browser = typeof globalThis.browser !== "undefined" ? globalThis.browser : chrome;

function getCurrentWindowTabs() {
return browser.tabs.query({ currentWindow: true, active: true });
Expand Down Expand Up @@ -103,19 +101,31 @@ document.addEventListener('DOMContentLoaded', async () => {

information.querySelector('#go-to-source-btn').addEventListener('click', async () => {
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
const currentUrl = tab.url;
const prefix = 'https://github.com/livekit/livekit_composite/blob/';
if (currentUrl.startsWith(prefix)) {
const rest = currentUrl.slice(prefix.length);
const match = rest.match(/^[^/]+\/([^/]+)\/([^/]+)\/(.+)$/);
if (match) {
const [, org, repo, filePath] = match;
const newUrl = `https://github.com/${org}/${repo}/blob/main/${filePath}`;
browser.tabs.update(tab.id, { url: newUrl });

// Sanity check: input must be livekit_composite
if (!tab.url.startsWith('https://github.com/livekit/livekit_composite')) {
alert('This is not a livekit_composite file URL.');
return;
}

const response = await browser.runtime.sendMessage({ type: 'convertCompositeUrl', url: tab.url });

if (response.url) {
// Sanity check: output must be github.com/livekit or github.com/livekit-examples before redirection
if (!response.url.startsWith('https://github.com/livekit/') &&
!response.url.startsWith('https://github.com/livekit-examples/')) {
alert('Internal Error: Invalid redirect URL generated.');
return;
}
// Use scripting API to navigate, preserving browser history
browser.scripting.executeScript({
target: { tabId: tab.id },
func: (url) => { location.assign(url); },
args: [response.url]
});
} else {
alert('Internal Error: could not parse the redirection url. This only works on file URLs (/blob/)');
}
alert('This is not a livekit_composite file URL or could not parse the path.');
});

information.querySelector('#ask-deepwiki-btn').addEventListener('click', () => {
Expand Down
Loading