(function (global) { 'use strict'; const STATE_KEY = 'convoreader_chat_git_v1'; const TOKEN_KEY = 'convoreader_github_token'; let syncQueue = Promise.resolve(); let commitQueue = Promise.resolve(); function initialState() { return { schemaVersion: 1, activeBranch: 'main', branches: { main: null }, commits: [], config: { owner: '', repo: '', githubBranch: 'main', autoSync: false }, sync: { status: 'disconnected', lastCommit: null, error: null } }; } function read() { try { const state = { ...initialState(), ...JSON.parse(localStorage.getItem(STATE_KEY) || '{}') }; state.branches = state.branches || { main: null }; state.commits = Array.isArray(state.commits) ? state.commits : []; state.config = { ...initialState().config, ...(state.config || {}) }; return state; } catch { return initialState(); } } function write(state) { localStorage.setItem(STATE_KEY, JSON.stringify(state)); global.dispatchEvent(new CustomEvent('chatgit:change', { detail: state })); return state; } async function hash(value) { const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(value))); return [...new Uint8Array(bytes)].map(v => v.toString(16).padStart(2, '0')).join(''); } function token() { return sessionStorage.getItem(TOKEN_KEY) || ''; } function safePart(value) { return String(value || 'stream').replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 100); } function utf8Base64(value) { const bytes = new TextEncoder().encode(value); let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary); } async function github(path, options = {}) { const state = read(); if (!token()) throw new Error('GitHub is not connected for this browser tab'); if (!state.config.owner || !state.config.repo) throw new Error('GitHub owner and repository are required'); const response = await fetch(`https://api.github.com/repos/${encodeURIComponent(state.config.owner)}/${encodeURIComponent(state.config.repo)}${path}`, { ...options, headers: { Accept: 'application/vnd.github+json', Authorization: `Bearer ${token()}`, 'X-GitHub-Api-Version': '2022-11-28', ...(options.headers || {}) } }); const data = response.status === 204 ? {} : await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.message || `GitHub HTTP ${response.status}`); return data; } async function connect({ owner, repo, githubBranch = 'main', accessToken, autoSync = true }) { if (!/^[A-Za-z0-9_.-]+$/.test(owner || '') || !/^[A-Za-z0-9_.-]+$/.test(repo || '')) throw new Error('Invalid repository identifier'); if (!accessToken) throw new Error('A fine-grained GitHub token is required'); sessionStorage.setItem(TOKEN_KEY, accessToken); const state = read(); state.config = { owner, repo, githubBranch, autoSync }; state.sync = { status: 'connecting', lastCommit: state.sync?.lastCommit || null, error: null }; write(state); try { const repository = await github(''); const next = read(); next.sync.status = 'connected'; next.sync.error = null; write(next); return { fullName: repository.full_name, defaultBranch: repository.default_branch, private: repository.private }; } catch (error) { sessionStorage.removeItem(TOKEN_KEY); const next = read(); next.sync = { status: 'error', lastCommit: next.sync?.lastCommit || null, error: error.message }; write(next); throw error; } } function disconnect() { sessionStorage.removeItem(TOKEN_KEY); const state = read(); state.sync.status = 'disconnected'; state.sync.error = null; return write(state); } async function commitNow({ streamId = 'workspace', type = 'chat', message, snapshot, codemap = null, parents = null }) { const state = read(); const branch = state.activeBranch; const body = { schemaVersion: 1, streamId, branch, type, message: String(message || type).slice(0, 240), parents: parents || (state.branches[branch] ? [state.branches[branch]] : []), snapshot, codemap, createdAt: new Date().toISOString() }; body.id = await hash(body); state.commits.push(body); if (state.commits.length > 2000) state.commits = state.commits.slice(-2000); state.branches[branch] = body.id; write(state); if (state.config.autoSync && token()) queueSync(streamId); return body; } function commit(input) { commitQueue = commitQueue.then(() => commitNow(input)); return commitQueue; } function createBranch(name, from = null) { const clean = safePart(name); const state = read(); if (state.branches[clean] !== undefined) throw new Error('Branch already exists'); state.branches[clean] = from || state.branches[state.activeBranch] || null; state.activeBranch = clean; return write(state); } function switchBranch(name) { const state = read(); if (state.branches[name] === undefined) throw new Error('Unknown branch'); state.activeBranch = name; return write(state); } async function merge(source, target = null, snapshot = null) { const state = read(); const destination = target || state.activeBranch; if (state.branches[source] === undefined || state.branches[destination] === undefined) throw new Error('Unknown branch'); const prior = state.activeBranch; state.activeBranch = destination; write(state); const result = await commit({ streamId: 'branch-merge', type: 'merge', message: `Merge ${source} into ${destination}`, snapshot: snapshot || { source, destination }, parents: [state.branches[destination], state.branches[source]].filter(Boolean) }); if (prior !== destination) switchBranch(prior); return result; } async function syncStream(streamId) { const state = read(); const streamCommits = state.commits.filter(c => c.streamId === streamId || c.type === 'merge'); const payload = { schemaVersion: 1, generatedAt: new Date().toISOString(), activeBranch: state.activeBranch, branches: state.branches, streamId, commits: streamCommits }; const filePath = `.convoreader/streams/${safePart(streamId)}.json`; const branch = state.config.githubBranch || 'main'; let existing = null; try { existing = await github(`/contents/${filePath}?ref=${encodeURIComponent(branch)}`); } catch (error) { if (!/not found/i.test(error.message)) throw error; } const latest = streamCommits.at(-1); const body = { message: `chat(${safePart(streamId)}): ${latest?.message || 'sync version graph'}`, content: utf8Base64(JSON.stringify(payload, null, 2) + '\n'), branch, ...(existing?.sha ? { sha: existing.sha } : {}) }; const result = await github(`/contents/${filePath}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const next = read(); next.sync = { status: 'connected', lastCommit: result.commit?.sha || null, error: null }; write(next); return result; } function queueSync(streamId) { syncQueue = syncQueue.then(() => syncStream(streamId)).catch(error => { const state = read(); state.sync = { status: 'error', lastCommit: state.sync?.lastCommit || null, error: error.message }; write(state); }); return syncQueue; } async function searchRepositories(query) { const response = await fetch(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=stars&order=desc&per_page=5`, { headers: { Accept: 'application/vnd.github+json', ...(token() ? { Authorization: `Bearer ${token()}` } : {}), 'X-GitHub-Api-Version': '2022-11-28' } }); const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.message || `GitHub search HTTP ${response.status}`); return { total: data.total_count || 0, items: (data.items || []).map(item => ({ name: item.full_name, url: item.html_url, stars: item.stargazers_count, description: item.description })) }; } global.ChatGit = { read, commit, connect, disconnect, createBranch, switchBranch, merge, syncStream, queueSync, searchRepositories, hasSessionToken: () => Boolean(token()) }; })(window);