mirror of
https://github.com/sune-org/store.git
synced 2026-01-13 16:17:58 +00:00
27 lines
15 KiB
JSON
27 lines
15 KiB
JSON
[
|
|
{
|
|
"id": "zp2je1g",
|
|
"name": "Github Fetch",
|
|
"pinned": false,
|
|
"avatar": "",
|
|
"url": "gh://sune-org/store@main/fetch.sune",
|
|
"updatedAt": 1756931228173,
|
|
"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": "<!-- GitHub Blob Fetch Sune (multi-URL with +) — compact, Fetch All on top -->\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 </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\">\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\" title=\"Add another URL\">\n <i data-lucide=\"plus\" class=\"h-4 w-4\"></i>\n </button>\n <button type=\"button\" id=\"ghHelpBtn\" class=\"text-[11px] px-2 py-1 rounded-lg bg-gray-100 hover:bg-gray-200\">Help</button>\n </div>\n </div>\n\n <div id=\"ghRows\" class=\"mt-2 space-y-1\"></div>\n\n <div class=\"mt-2\">\n <div id=\"ghStatus\" class=\"text-[11px] text-gray-600\"></div>\n </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 {} };\n\n // Storage keys (migrate from single URL if present)\n function storageKeyBase() {\n const t = document.getElementById('settingsBtnTop')?.title || '';\n const m = t.match(/Settings — (.+)$/);\n const name = (m ? m[1] : 'default').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');\n return 'gh_fetch_' + name;\n }\n const KEY_SINGLE = () => storageKeyBase(); // old single value\n const KEY_MULTI = () => storageKeyBase() + '_v2'; // new array value\n\n function loadUrls() {\n try {\n const multi = localStorage.getItem(KEY_MULTI());\n if (multi) {\n const arr = JSON.parse(multi);\n if (Array.isArray(arr)) return arr.map(x => String(x||''));\n }\n const old = localStorage.getItem(KEY_SINGLE());\n if (old && typeof old === 'string') return [old];\n } catch {}\n return [''];\n }\n\n function saveUrls(urls) {\n try {\n localStorage.setItem(KEY_MULTI(), JSON.stringify(urls.map(u => String(u||'').trim())));\n } catch {}\n }\n\n function setGlobalStatus(msg, kind = 'info') {\n els.status.textContent = msg || '';\n els.status.className = 'text-[11px] ' + (kind === 'error' ? 'text-red-600' : kind === 'ok' ? 'text-green-600' : 'text-gray-600');\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 // URL parsing and helpers\n function normalizeNewlines(s) { return s.replace(/\\r\\n/g, '\\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 // parse #Lx or #Lx-Ly\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 // owner/repo/blob/raw/ref/...path\n if (parts.length >= 5 && (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.origin + url.pathname;\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 name = p.toLowerCase();\n const ext = (name.split('.').pop() || '').trim();\n if (/\\.dockerfile$/.test(name) || /(^|\\/)dockerfile$/.test(name)) return 'dockerfile';\n if (/(^|\\/)makefile$/.test(name)) return 'makefile';\n const map = {\n js: 'javascript', mjs: 'javascript', cjs: 'javascript',\n ts: 'typescript', tsx: 'tsx', jsx: 'jsx',\n json: 'json', md: 'markdown',\n py: 'python', rb: 'ruby', go: 'go', rs: 'rust',\n java: 'java', kt: 'kotlin', swift: 'swift',\n c: 'c', h: 'c', cc: 'cpp', cpp: 'cpp', cxx: 'cpp', hpp: 'cpp', hh: 'cpp',\n cs: 'csharp', php: 'php',\n sh: 'bash', bash: 'bash', zsh: 'bash',\n env: 'dotenv', dotenv: 'dotenv',\n yml: 'yaml', yaml: 'yaml',\n toml: 'ini', ini: 'ini', cfg: 'ini',\n sql: 'sql', html: 'html', htm: 'html',\n css: 'css', scss: 'scss', less: 'less',\n vue: 'vue', svelte: 'svelte',\n gradle: 'groovy', groovy: 'groovy',\n txt: '', lock: '', log: ''\n };\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 const res = await fetch(parsed.raw, { cache: 'no-store' });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n let content = await res.text();\n content = normalizeNewlines(content);\n\n if (parsed.lines) {\n const lines = content.split('\\n');\n const s = Math.max(1, parsed.lines.start), e = Math.max(s, parsed.lines.end);\n content = lines.slice(s - 1, 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 // Row rendering\n let urls = loadUrls();\n\n function rowTpl(idx, value='') {\n const id = `ghRow_${idx}`;\n return `\n <div class=\"rounded-lg border border-gray-200 p-2\">\n <div class=\"flex items-stretch gap-2\">\n <input\n data-role=\"url\"\n data-index=\"${idx}\"\n id=\"${id}\"\n type=\"url\"\n inputmode=\"url\"\n placeholder=\"https://github.com/owner/repo/blob/branch/file#L1-L20\"\n class=\"flex-1 rounded-md border border-gray-300 px-2 py-1 text-xs\"\n aria-label=\"GitHub blob or raw URL\"\n value=\"${(value || '').replace(/\"/g, '"')}\"\n />\n <button data-role=\"paste\" data-index=\"${idx}\" type=\"button\" class=\"rounded-md border px-2 py-1 text-xs bg-white hover:bg-gray-50\" title=\"Paste from clipboard\">\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=\"rounded-md px-2 py-1 text-xs bg-black text-white hover:bg-black/90\">\n Fetch\n </button>\n <button data-role=\"remove\" data-index=\"${idx}\" type=\"button\" class=\"rounded-md border px-2 py-1 text-xs bg-white hover:bg-gray-50\" 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\"></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 node.className = 'mt-1 text-[11px] ' + (kind === 'error' ? 'text-red-600' : kind === 'ok' ? 'text-green-600' : 'text-gray-500');\n }\n\n async function postToChat(md) {\n if (window.USER && typeof window.USER.log === 'function') {\n await window.USER.log(md);\n return true;\n }\n throw new Error('window.USER.log is not available');\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, 'Enter a GitHub blob or raw URL.', 'error');\n return;\n }\n\n fetchBtn.dataset._prev = fetchBtn.innerHTML;\n fetchBtn.innerHTML = spinnerSvg();\n fetchBtn.classList.add('bg-gray-800');\n input.disabled = true;\n\n try {\n const { md, blob } = await fetchOne(url);\n await postToChat(md);\n urls[index] = blob; // store canonical blob\n saveUrls(urls);\n setRowStatus(index, 'Added to chat.', 'ok');\n } catch (err) {\n setRowStatus(index, (err && err.message) ? err.message : String(err), 'error');\n } finally {\n input.disabled = false;\n if (fetchBtn.dataset._prev) fetchBtn.innerHTML = fetchBtn.dataset._prev;\n fetchBtn.classList.remove('bg-gray-800');\n }\n }\n\n // Events - delegation for rows\n els.rows.addEventListener('click', async (e) => {\n const btn = e.target.closest('button[data-role]');\n if (!btn) return;\n const role = btn.getAttribute('data-role');\n const index = +btn.getAttribute('data-index');\n if (Number.isNaN(index)) return;\n\n if (role === 'paste') {\n try {\n const t = await navigator.clipboard.readText();\n if (t) {\n const input = els.rows.querySelector(`input[data-role=\"url\"][data-index=\"${index}\"]`);\n input.value = t.trim();\n urls[index] = input.value;\n saveUrls(urls);\n setRowStatus(index, 'Pasted from clipboard.');\n }\n } catch {\n setRowStatus(index, 'Clipboard read failed. Paste manually.', 'error');\n }\n } else if (role === 'fetch-one') {\n await fetchRow(index);\n } else if (role === 'remove') {\n if (urls.length === 1) {\n urls[0] = '';\n } else {\n urls.splice(index, 1);\n }\n saveUrls(urls);\n renderRows();\n }\n });\n\n els.rows.addEventListener('change', (e) => {\n const input = e.target.closest('input[data-role=\"url\"]');\n if (!input) return;\n const index = +input.getAttribute('data-index');\n if (Number.isNaN(index)) return;\n urls[index] = input.value.trim();\n saveUrls(urls);\n });\n\n els.rows.addEventListener('keydown', (e) => {\n const input = e.target.closest('input[data-role=\"url\"]');\n if (!input) return;\n if (e.key === 'Enter') {\n e.preventDefault();\n const index = +input.getAttribute('data-index');\n if (!Number.isNaN(index)) fetchRow(index);\n }\n });\n\n // Top controls\n els.addBtn.addEventListener('click', () => {\n urls.push('');\n saveUrls(urls);\n renderRows();\n requestAnimationFrame(() => {\n const lastIndex = urls.length - 1;\n const input = els.rows.querySelector(`input[data-role=\"url\"][data-index=\"${lastIndex}\"]`);\n input?.focus();\n });\n });\n\n els.fetchAllBtn.addEventListener('click', async () => {\n const list = urls.map((u, i) => ({ url: (u||'').trim(), i })).filter(x => x.url);\n if (!list.length) {\n setGlobalStatus('No URLs to fetch. Add with + or paste into a field.', 'error');\n return;\n }\n\n els.fetchAllBtn.dataset._prev = els.fetchAllBtn.innerHTML;\n els.fetchAllBtn.innerHTML = spinnerSvg('h-3 w-3');\n els.fetchAllBtn.classList.add('bg-gray-800');\n els.fetchAllBtn.disabled = true;\n setGlobalStatus('Fetching…');\n\n let ok = 0, fail = 0;\n for (const it of list) {\n try {\n await fetchRow(it.i);\n ok++;\n } catch {\n fail++;\n }\n }\n\n els.fetchAllBtn.innerHTML = els.fetchAllBtn.dataset._prev || 'Fetch All';\n els.fetchAllBtn.classList.remove('bg-gray-800');\n els.fetchAllBtn.disabled = false;\n\n if (fail === 0) setGlobalStatus(`Fetched ${ok} item(s).`, 'ok');\n else setGlobalStatus(`Fetched ${ok}, ${fail} failed.`, 'error');\n });\n\n els.helpBtn.addEventListener('click', () => {\n const tip = [\n 'How to use:',\n '• Click + to add more URL fields.',\n '• Enter GitHub blob URLs, e.g.:',\n ' https://github.com/owner/repo/blob/branch/path/to/file.ext#L10-L42',\n ' or raw URLs like:',\n ' https://raw.githubusercontent.com/owner/repo/branch/path/to/file.ext',\n '• Click Fetch on a row (or Fetch All) to post into the chat.',\n '',\n 'Notes:',\n '- #Lx or #Lx-Ly anchors will select line(s).',\n '- Private repos are not supported.',\n '- URLs are stored locally per sune.'\n ].join('\\n');\n alert(tip);\n });\n\n // Initial render\n function setGlobalStatus(msg, kind = 'info') {\n els.status.textContent = msg || '';\n els.status.className = 'text-[11px] ' + (kind === 'error' ? 'text-red-600' : kind === 'ok' ? 'text-green-600' : 'text-gray-600');\n }\n\n renderRows();\n icons();\n})();\n</script>",
|
|
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />"
|
|
}
|
|
}
|
|
] |