mirror of
https://github.com/sune-org/store.git
synced 2026-01-14 00:27:59 +00:00
28 lines
14 KiB
JSON
28 lines
14 KiB
JSON
[
|
|
{
|
|
"id": "zp2je1g",
|
|
"name": "Github Fetch",
|
|
"pinned": false,
|
|
"avatar": "",
|
|
"url": "gh://sune-org/store@main/fetch.sune",
|
|
"updatedAt": 1757022561032,
|
|
"settings": {
|
|
"model": "openai/gpt-5",
|
|
"temperature": 1,
|
|
"top_p": 0.96,
|
|
"top_k": 0,
|
|
"frequency_penalty": 0,
|
|
"presence_penalty": 0,
|
|
"repetition_penalty": 1,
|
|
"min_p": 0,
|
|
"top_a": 0,
|
|
"max_tokens": 0,
|
|
"verbosity": "",
|
|
"reasoning_effort": "default",
|
|
"system_prompt": "",
|
|
"html": "<!-- Sune: GitHub Blob Fetch -->\n<!-- Version: 1.1.0 -->\n<div id=\"ghFetchSune\" class=\"p-2 border-b border-gray-200\">\n <div class=\"flex items-center justify-between\">\n <div class=\"flex items-center gap-2\">\n <i data-lucide=\"github\" class=\"h-4 w-4\"></i>\n <span class=\"text-xs font-medium\">GitHub Fetch</span>\n <span class=\"text-[10px] text-gray-400\">v1.1.0</span>\n </div>\n <div class=\"flex items-center gap-1\">\n <button id=\"ghFetchAllBtn\" type=\"button\" class=\"rounded-lg px-2 py-1 text-xs bg-black text-white hover:bg-black/90 active:scale-95 transition-transform\">\n Fetch All\n </button>\n <button type=\"button\" id=\"ghAddRowBtn\" class=\"h-8 w-8 rounded-lg bg-gray-100 hover:bg-gray-200 flex items-center justify-center active:scale-95 transition-transform\" title=\"Add another URL\">\n <i data-lucide=\"plus\" class=\"h-4 w-4\"></i>\n </button>\n <button type=\"button\" id=\"ghHelpBtn\" class=\"h-8 px-2 rounded-lg bg-gray-100 hover:bg-gray-200 flex items-center justify-center active:scale-95 transition-transform\">\n <i data-lucide=\"help-circle\" class=\"h-4 w-4\"></i>\n </button>\n </div>\n </div>\n\n <div id=\"ghRows\" class=\"mt-2 space-y-1\"></div>\n <div id=\"ghStatus\" class=\"mt-2 text-[11px] text-gray-600 min-h-[1em]\"></div>\n</div>\n\n<script>\n(() => {\n const root = document.getElementById('ghFetchSune');\n if (!root) return;\n\n const $ = (sel, ctx = root) => ctx.querySelector(sel);\n const $$ = (sel, ctx = root) => Array.from(ctx.querySelectorAll(sel));\n\n const els = {\n rows: $('#ghRows'),\n addBtn: $('#ghAddRowBtn'),\n fetchAllBtn: $('#ghFetchAllBtn'),\n helpBtn: $('#ghHelpBtn'),\n status: $('#ghStatus'),\n };\n\n const icons = () => { try { window.lucide && window.lucide.createIcons({ attrs: { 'aria-hidden': 'true' } }); } catch (e) { console.error(\"Lucide error:\", e); } };\n\n function storageKeyBase() {\n // Use the active Sune's unique ID for robust, non-conflicting storage.\n const suneId = window.SUNE?.id;\n if (suneId) {\n return `gh_fetch_sune_${suneId}`;\n }\n // Fallback for safety, though SUNE.id should always exist.\n const name = (window.SUNE?.name || 'default').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');\n return `gh_fetch_${name}`;\n }\n \n const getUrlsKey = () => storageKeyBase() + '_urls_v2';\n\n function loadUrls() {\n try {\n const multi = window.localStorage.getItem(getUrlsKey());\n if (multi) {\n const arr = JSON.parse(multi);\n return Array.isArray(arr) ? arr.map(x => String(x || '')) : [''];\n }\n } catch {}\n return [''];\n }\n\n function saveUrls(urls) {\n try {\n window.localStorage.setItem(getUrlsKey(), JSON.stringify(urls.map(u => String(u || '').trim())));\n } catch {}\n }\n\n function setGlobalStatus(msg, kind = 'info') {\n els.status.textContent = msg || '';\n const colorClass = kind === 'error' ? 'text-red-600' : kind === 'ok' ? 'text-green-600' : 'text-gray-600';\n els.status.className = `mt-2 text-[11px] min-h-[1em] ${colorClass}`;\n }\n\n function spinnerSvg(cls='h-3 w-3') {\n return `<svg class=\"animate-spin ${cls}\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" fill=\"none\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v4A4 4 0 004 12z\"></path></svg>`;\n }\n\n function parseGitHubUrl(u) {\n try {\n const url = new URL(u);\n let owner, repo, ref, path, raw, blob, lines = null;\n\n if (url.hash && /#L\\d+/i.test(url.hash)) {\n const m = url.hash.match(/#L(\\d+)(?:-L(\\d+))?/i);\n if (m) lines = { start: Math.max(1, +m[1] || 1), end: Math.max(+m[1] || 1, +m[2] || +m[1] || 1) };\n }\n\n if (url.hostname === 'github.com') {\n const parts = url.pathname.split('/').filter(Boolean);\n if (parts.length >= 4 && (parts[2] === 'blob' || parts[2] === 'raw')) {\n owner = parts[0]; repo = parts[1]; ref = parts[3];\n path = parts.slice(4).join('/');\n raw = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`;\n blob = `https://github.com/${owner}/${repo}/blob/${ref}/${path}${url.hash || ''}`;\n return { raw, blob, owner, repo, ref, path, lines };\n }\n } else if (url.hostname === 'raw.githubusercontent.com') {\n const parts = url.pathname.split('/').filter(Boolean);\n if (parts.length >= 4) {\n owner = parts[0]; repo = parts[1]; ref = parts[2];\n path = parts.slice(3).join('/');\n raw = url.href;\n blob = `https://github.com/${owner}/${repo}/blob/${ref}/${path}${url.hash || ''}`;\n return { raw, blob, owner, repo, ref, path, lines };\n }\n }\n return null;\n } catch { return null; }\n }\n \n function guessLangFromPath(p = '') {\n const ext = (p.split('.').pop() || '').toLowerCase();\n if (!ext) return '';\n const map = { js: 'javascript', ts: 'typescript', py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java', kt: 'kotlin', cs: 'csharp', php: 'php', sh: 'bash', ps1: 'powershell', sql: 'sql', html: 'html', css: 'css', scss: 'scss', md: 'markdown', json: 'json', yml: 'yaml', yaml: 'yaml', toml: 'toml', Dockerfile: 'dockerfile' };\n return map[ext] || '';\n }\n\n async function fetchOne(rawInput) {\n const input = (rawInput || '').trim();\n if (!input) throw new Error('Empty URL');\n const parsed = parseGitHubUrl(input);\n if (!parsed) throw new Error('Invalid GitHub URL. Use blob/raw format.');\n\n const fetchOptions = { cache: 'no-store' };\n const ghToken = window.globalStore?.ghToken;\n if (ghToken) {\n fetchOptions.headers = { 'Authorization': `Bearer ${ghToken}` };\n }\n\n const res = await fetch(parsed.raw, fetchOptions);\n if (!res.ok) throw new Error(`Fetch failed: HTTP ${res.status}`);\n \n let content = await res.text();\n if (parsed.lines) {\n const lines = content.replace(/\\r\\n/g, '\\n').split('\\n');\n const s = parsed.lines.start - 1, e = parsed.lines.end;\n content = lines.slice(s, e).join('\\n');\n }\n\n const lang = guessLangFromPath(parsed.path);\n const header = `From: ${parsed.blob}`;\n const md = `${header}\\n\\n\\`\\`\\`${lang}\\n${content}\\n\\`\\`\\``;\n return { md, blob: parsed.blob };\n }\n\n let urls = loadUrls();\n\n function rowTpl(idx, value = '') {\n return `\n <div class=\"rounded-lg border border-gray-200 bg-white p-2\">\n <div class=\"flex items-stretch gap-2\">\n <input\n data-role=\"url\" data-index=\"${idx}\" type=\"url\" inputmode=\"url\"\n placeholder=\"GitHub blob or raw URL...\"\n class=\"flex-1 min-w-0 rounded-md border border-gray-300 px-2 py-1 text-xs focus:ring-2 focus:ring-black/20 focus:border-black transition\"\n aria-label=\"GitHub URL\" value=\"${value.replace(/\"/g, '"')}\"\n />\n <button data-role=\"paste\" data-index=\"${idx}\" type=\"button\" class=\"shrink-0 rounded-md border px-2 text-xs bg-white hover:bg-gray-50 active:scale-95 transition-transform\" title=\"Paste\">\n <i data-lucide=\"clipboard\" class=\"h-4 w-4\"></i>\n </button>\n <button data-role=\"fetch-one\" data-index=\"${idx}\" type=\"button\" class=\"shrink-0 rounded-md px-2 text-xs bg-black text-white hover:bg-black/90 active:scale-95 transition-transform\">\n Fetch\n </button>\n <button data-role=\"remove\" data-index=\"${idx}\" type=\"button\" class=\"shrink-0 rounded-md border p-2 text-xs text-gray-500 bg-white hover:bg-gray-50 hover:text-red-600 active:scale-95 transition-transform\" title=\"Remove\">\n <i data-lucide=\"trash-2\" class=\"h-4 w-4\"></i>\n </button>\n </div>\n <div data-role=\"row-status\" data-index=\"${idx}\" class=\"mt-1 text-[11px] text-gray-500 min-h-[1em]\"></div>\n </div>\n `;\n }\n\n function renderRows() {\n if (!urls.length) urls = [''];\n els.rows.innerHTML = urls.map((u, i) => rowTpl(i, u)).join('');\n icons();\n }\n \n function setRowStatus(index, msg, kind='info') {\n const node = els.rows.querySelector(`[data-role=\"row-status\"][data-index=\"${index}\"]`);\n if (!node) return;\n node.textContent = msg || '';\n const colorClass = kind === 'error' ? 'text-red-600' : kind === 'ok' ? 'text-green-600' : 'text-gray-500';\n node.className = `mt-1 text-[11px] min-h-[1em] ${colorClass}`;\n }\n\n async function postToChat(md) {\n if (window.USER && typeof window.USER.log === 'function') {\n await window.USER.log(md);\n } else {\n throw new Error('Chat injection function (USER.log) not found.');\n }\n }\n\n async function fetchRow(index) {\n const input = els.rows.querySelector(`input[data-role=\"url\"][data-index=\"${index}\"]`);\n const fetchBtn = els.rows.querySelector(`button[data-role=\"fetch-one\"][data-index=\"${index}\"]`);\n if (!input || !fetchBtn) return;\n\n setRowStatus(index, '');\n const url = (input.value || '').trim();\n if (!url) {\n setRowStatus(index, 'Please enter a GitHub URL.', 'error');\n return false;\n }\n\n const prevHTML = fetchBtn.innerHTML;\n fetchBtn.innerHTML = spinnerSvg();\n fetchBtn.disabled = true;\n input.disabled = true;\n\n try {\n const { md, blob } = await fetchOne(url);\n await postToChat(md);\n input.value = blob; // Store canonical blob URL\n urls[index] = blob;\n saveUrls(urls);\n setRowStatus(index, 'Added to chat.', 'ok');\n return true;\n } catch (err) {\n setRowStatus(index, err.message || String(err), 'error');\n return false;\n } finally {\n fetchBtn.innerHTML = prevHTML;\n fetchBtn.disabled = false;\n input.disabled = false;\n }\n }\n\n els.rows.addEventListener('click', async (e) => {\n const btn = e.target.closest('button[data-role]');\n if (!btn) return;\n const role = btn.dataset.role;\n const index = +btn.dataset.index;\n if (Number.isNaN(index)) return;\n\n if (role === 'paste') {\n try {\n const text = await navigator.clipboard.readText();\n const input = els.rows.querySelector(`input[data-index=\"${index}\"]`);\n if (input && text) {\n input.value = text.trim();\n urls[index] = input.value;\n saveUrls(urls);\n setRowStatus(index, 'Pasted.', 'ok');\n }\n } catch {\n setRowStatus(index, 'Clipboard read failed.', 'error');\n }\n } else if (role === 'fetch-one') {\n await fetchRow(index);\n } else if (role === 'remove') {\n urls.length > 1 ? urls.splice(index, 1) : (urls[0] = '');\n saveUrls(urls);\n renderRows();\n }\n });\n\n els.rows.addEventListener('input', (e) => {\n const input = e.target.closest('input[data-role=\"url\"]');\n if (!input) return;\n urls[+input.dataset.index] = input.value;\n saveUrls(urls);\n });\n \n els.rows.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && e.target.matches('input[data-role=\"url\"]')) {\n e.preventDefault();\n fetchRow(+e.target.dataset.index);\n }\n });\n\n els.addBtn.addEventListener('click', () => {\n urls.push('');\n saveUrls(urls);\n renderRows();\n els.rows.querySelector(`input[data-index=\"${urls.length - 1}\"]`)?.focus();\n });\n\n els.fetchAllBtn.addEventListener('click', async () => {\n const list = urls.map((url, i) => ({ url: url.trim(), i })).filter(item => item.url);\n if (!list.length) {\n setGlobalStatus('No URLs to fetch. Add one with the + button.', 'error');\n return;\n }\n\n const prevHTML = els.fetchAllBtn.innerHTML;\n els.fetchAllBtn.innerHTML = spinnerSvg('h-4 w-4');\n els.fetchAllBtn.disabled = true;\n setGlobalStatus(`Fetching ${list.length} item(s)...`);\n\n let success = 0, failed = 0;\n for (const item of list) {\n const ok = await fetchRow(item.i);\n ok ? success++ : failed++;\n }\n\n els.fetchAllBtn.innerHTML = prevHTML;\n els.fetchAllBtn.disabled = false;\n \n if (failed === 0) {\n setGlobalStatus(`Fetched ${success} item(s) successfully.`, 'ok');\n } else {\n setGlobalStatus(`Completed: ${success} succeeded, ${failed} failed.`, 'error');\n }\n });\n\n els.helpBtn.addEventListener('click', () => {\n alert([\n 'GitHub Fetch Sune - Help',\n '',\n '• Use the + button to add URL fields.',\n '• Paste a GitHub \"blob\" or \"raw\" URL into a field.',\n '• Example: https://github.com/owner/repo/blob/main/path/file.js#L10-L20',\n '• The #L... part is optional and selects specific lines.',\n '• Click \"Fetch\" on a row, or \"Fetch All\" for all fields.',\n '',\n 'Pro Tip: Go to Account Settings and add your GitHub Token to fetch from private repos and avoid rate limits.',\n ].join('\\n'));\n });\n\n renderRows();\n})();\n</script>\n",
|
|
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />"
|
|
}
|
|
}
|
|
]
|