mirror of
https://github.com/sune-org/store.git
synced 2026-01-14 16:48:14 +00:00
1 line
24 KiB
JSON
1 line
24 KiB
JSON
[{"id":"zp2je1g","name":"Github Fetch","pinned":false,"avatar":"data:image/webp;base64,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","url":"gh://sune-org/store/github-utilities/fetch.sune","updatedAt":1762637582141,"settings":{"model":"","temperature":"","top_p":"","top_k":"","frequency_penalty":"","repetition_penalty":"","min_p":"","top_a":"","verbosity":"","reasoning_effort":"default","system_prompt":"","html":"<div id=\"ghFetchSune\" class=\"p-1.5 bg-slate-100 rounded-lg shadow-sm font-sans\" x-data>\n <div class=\"flex items-center justify-between\">\n <div class=\"flex items-center gap-1.5\">\n <i data-lucide=\"github\" class=\"h-4 w-4 text-slate-600\"></i>\n <span class=\"text-xs font-medium text-slate-800\">GitHub Fetch</span>\n <span class=\"text-[10px] text-slate-400\">v1.9.4</span>\n </div>\n <div class=\"flex items-center gap-1\">\n <button id=\"ghFetchAllBtn\" type=\"button\" class=\"rounded-md px-2 py-1 text-xs font-semibold bg-slate-800 text-white hover:bg-slate-700 active:scale-95 transition-all\">\n Fetch All\n </button>\n <button type=\"button\" id=\"ghAddRowBtn\" class=\"h-7 w-7 rounded-md bg-slate-200/80 text-slate-600 hover:bg-white active:shadow-inner active:scale-95 flex items-center justify-center transition-all\" title=\"Add URL\">\n <i data-lucide=\"plus\" class=\"h-4 w-4\"></i>\n </button>\n <!-- Help button removed -->\n </div>\n </div>\n\n <div id=\"ghRows\" class=\"mt-2 space-y-1.5\"></div>\n <div id=\"ghStatus\" class=\"mt-1.5 px-1 text-[11px] text-slate-600 min-h-[1em]\"></div>\n</div>\n\n<script>\n(()=>{\n const root=document.getElementById('ghFetchSune');\n if(!root)return;\n\n const $=s=>root.querySelector(s);\n\n const els={\n rows:$('#ghRows'),\n add:$('#ghAddRowBtn'),\n all:$('#ghFetchAllBtn'),\n status:$('#ghStatus')\n };\n\n const cache={};\n\n const icons=()=>{\n try{\n lucide.createIcons({attrs:{'aria-hidden':!0}});\n }catch(e){}\n };\n\n const getKey=()=> 'gh_fetch_sune_shared_urls_v1';\n const getIgnoreKey=()=> 'gh_fetch_sune_shared_ignores_v1';\n\n const load=()=>{\n try{\n const d=JSON.parse(localStorage.getItem(getKey()));\n return Array.isArray(d)?d.map(x=>String(x||'')):[''];\n }catch{\n return[''];\n }\n };\n\n const loadIgnores=()=>{\n try{\n const d=JSON.parse(localStorage.getItem(getIgnoreKey()));\n return Array.isArray(d)?d.map(x=>String(x||'')):[];\n }catch{\n return[];\n }\n };\n\n const save=d=>{\n localStorage.setItem(\n getKey(),\n JSON.stringify(d.map(u=>(u||'').trim()))\n );\n };\n\n const saveIgnores=ig=>{\n localStorage.setItem(\n getIgnoreKey(),\n JSON.stringify(ig.map(v=>String(v||'')))\n );\n };\n\n const setStatus=(el,msg,kind)=>{\n const k={\n error:'text-red-600',\n ok:'text-emerald-600',\n info:el.id==='ghStatus'\n ?'text-slate-600'\n :'text-slate-500'\n };\n el.textContent=msg||'';\n el.className=`${el.dataset.baseClass} ${k[kind]||k.info}`;\n };\n\n const setGlobalStatus=(m,k='info')=>setStatus(els.status,m,k);\n\n const setRowStatus=(i,m,k='info')=>{\n const n=$(\n `[data-role=\"row-status\"][data-index=\"${i}\"]`\n );\n if(n)setStatus(n,m,k);\n };\n\n const spinner=c=>`\n <svg class=\"animate-spin ${c}\" viewBox=\"0 0 24 24\">\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n fill=\"none\">\n </circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8v4A4 4 0 004 12z\">\n </path>\n </svg>\n `;\n\n const ghFetch=async(url,opts={})=>{\n const h=opts.headers||{};\n if(window.globalStore?.ghToken){\n h.Authorization=`Bearer ${window.globalStore.ghToken}`;\n }\n const res=await fetch(url,{...opts,headers:h});\n if(!res.ok)throw new Error(`Request failed: HTTP ${res.status}`);\n return res;\n };\n\n const getInfo=async(o,r)=>{\n const k=`${o}/${r}`;\n if(cache[k])return cache[k];\n const d=await(\n await ghFetch(`https://api.github.com/repos/${o}/${r}`)\n ).json();\n if(!d.default_branch)throw new Error('No default branch.');\n return cache[k]=d;\n };\n\n async function parse(u){\n try{\n const lM=u.match(/#L(\\d+)(?:-L(\\d+))?$/i);\n const lines=lM\n ?{start:+lM[1],end:+lM[2]||+lM[1]}\n :null;\n const urlStr=u.split('#')[0];\n\n try{\n const url=new URL(urlStr);\n let owner,repo,ref,path;\n\n if(url.hostname==='github.com'){\n const p=url.pathname\n .split('/')\n .filter(Boolean);\n if(p.length>=4&&(p[2]==='blob'||p[2]==='raw')){\n [owner,repo,,ref]=p;\n path=p.slice(4).join('/');\n return{\n type:'file',\n p:{\n owner,\n repo,\n ref,\n path,\n lines,\n raw:`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`\n }\n };\n }\n }else if(url.hostname==='raw.githubusercontent.com'){\n const p=url.pathname\n .split('/')\n .filter(Boolean);\n if(p.length>=4){\n [owner,repo,ref]=p;\n path=p.slice(3).join('/');\n return{\n type:'file',\n p:{\n owner,\n repo,\n ref,\n path,\n lines,\n raw:url.href\n }\n };\n }\n }\n }catch(e){}\n\n const p=urlStr.split('/').filter(Boolean);\n if(p.length>=2){\n const[owner,repo]=p;\n if(p.length>2){\n const path=p.slice(2).join('/');\n const{default_branch:ref}=await getInfo(owner,repo);\n return{\n type:'file',\n p:{\n owner,\n repo,\n ref,\n path,\n lines,\n raw:`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`\n }\n };\n }\n const info=await getInfo(owner,repo);\n return{\n type:'repo',\n p:{\n owner,\n repo,\n default_branch:info.default_branch\n }\n };\n }\n }catch(e){\n return null;\n }\n }\n\n const isBin=p=>/\\.(png|jpe?g|gif|bmp|webp|ico|tif?f|svg|zip|rar|7z|tar|gz|bz2|xz|pdf|docx?|xlsx?|pptx?|mp[34]|wav|ogg|webm|mov|avi|mkv|woff2?|ttf|otf|eot|exe|dll|so|dylib|bin|o|a|class|jar|pyc|iso|img|dmg)$/i\n .test(p);\n\n const getLang=p=>(p.split('.').pop()||'')\n .toLowerCase();\n\n const getFence=s=>'`'.repeat(\n Math.max(\n 3,\n 1+Math.max(\n 0,\n ...(s.match(/`+/g)||[])\n .map(x=>x.length)\n )\n )\n );\n\n const fetchFile=async({raw,path,lines,owner,repo,ref})=>{\n const h=`[${owner}/${repo}@${ref}/${path}](https://github.com/${owner}/${repo}/blob/${ref}/${path})`;\n\n if(isBin(path)){\n return{\n md:`${h}\\n\\n*Binary file - content not displayed.*`\n };\n }\n\n let content=await(\n await ghFetch(raw,{cache:'no-store'})\n ).text();\n\n let fileContentMd;\n\n if(path.toLowerCase().endsWith('.sune')){\n try{\n const d=JSON.parse(content);\n const html=d?.[0]?.settings?.html;\n if(typeof html==='string'){\n const fence=getFence(html);\n fileContentMd=`${fence}html\\n${html}\\n${fence}`;\n }\n }catch(e){}\n }\n\n if(!fileContentMd){\n if(lines){\n content=content\n .replace(/\\r\\n/g,'\\n')\n .split('\\n')\n .slice(lines.start-1,lines.end)\n .join('\\n');\n }\n const fence=getFence(content);\n fileContentMd=`${fence}${getLang(path)}\\n${content}\\n${fence}`;\n }\n\n return{\n md:`${h}\\n\\n${fileContentMd}`\n };\n };\n\n const matchIgnore=(path,patterns)=>{\n if(!patterns?.length)return!1;\n let included=null;\n\n for(const raw of patterns){\n const s=(raw||'').trim();\n if(!s||s.startsWith('#'))continue;\n\n if(s==='*'){\n included=false;\n continue;\n }\n\n const negative=s[0]==='!';\n const pat=negative?s.slice(1):s;\n if(!pat)continue;\n\n let q=pat.replace(\n /[.+^${}()|[\\]\\\\]/g,\n '\\\\$&'\n );\n\n q=q.replace(/\\\\\\*\\\\\\*/g,'.*');\n q=q.replace(/\\\\\\*/g,'[^/]*');\n\n if(pat.endsWith('/'))q=`^${q}.*`;\n else if(pat.startsWith('/'))q=`^${q}$`;\n else q=`(^|/)${q}($|/)`;\n\n const re=new RegExp(q);\n\n if(negative){\n if(re.test(path))included=false;\n }else if(re.test(path)){\n included=true;\n }\n }\n\n return included===true;\n };\n\n const runPool=async(jobs,limit,onProgress)=>{\n const out=new Array(jobs.length);\n let next=0;\n let active=0;\n\n return new Promise(resolve=>{\n const kick=()=>{\n if(next>=jobs.length && !active){\n resolve(out);\n return;\n }\n while(active<limit && next<jobs.length){\n const i=next++;\n const job=jobs[i];\n active++;\n job()\n .then(r=>{out[i]=r;})\n .catch(e=>{out[i]={error:e};})\n .finally(()=>{\n active--;\n onProgress&&onProgress(i,out[i]);\n kick();\n });\n }\n };\n kick();\n });\n };\n\n const post=async md=>{\n if(window.USER?.logMany && Array.isArray(md)){\n return window.USER.logMany(md);\n }\n if(window.USER?.log){\n if(Array.isArray(md)){\n for(const m of md){\n await window.USER.log(m);\n }\n return;\n }\n return window.USER.log(md);\n }\n return new Error('Chat injection failed.');\n };\n\n const fetchRepo=async({owner,repo,default_branch},i,ignoreStr)=>{\n setRowStatus(i,'Fetching tree...');\n\n const{\n commit:{sha}\n }=await(\n await ghFetch(\n `https://api.github.com/repos/${owner}/${repo}/branches/${default_branch}`\n )\n ).json();\n\n const{\n tree,\n truncated\n }=await(\n await ghFetch(\n `https://api.github.com/repos/${owner}/${repo}/git/trees/${sha}?recursive=1`\n )\n ).json();\n\n if(truncated)throw new Error('Repo too large to fetch completely.');\n\n const allFiles=tree.filter(n=>n.type==='blob');\n if(!allFiles.length)throw new Error('No files found in repo.');\n\n const ignorePatterns=(ignoreStr||'')\n .split('\\n')\n .map(s=>s.trim())\n .filter(Boolean);\n\n const targets=[];\n for(const f of allFiles){\n const path=f.path;\n if(matchIgnore(path,ignorePatterns)){\n continue;\n }\n targets.push({path});\n }\n\n if(!targets.length){\n throw new Error('All files ignored by patterns.');\n }\n\n const total=targets.length;\n let completed=0;\n let posted=0;\n let skipped=allFiles.length-total;\n\n const jobs=targets.map(({path})=>async()=>{\n const h=`[${owner}/${repo}@${default_branch}/${path}](https://github.com/${owner}/${repo}/blob/${default_branch}/${path})`;\n\n if(isBin(path)){\n return{\n ok:true,\n md:`${h}\\n\\n*Binary file - content not displayed.*`,\n path\n };\n }\n\n try{\n let content=await(\n await ghFetch(\n `https://raw.githubusercontent.com/${owner}/${repo}/${sha}/${path}`\n )\n ).text();\n\n let fileContentMd;\n\n if(path.toLowerCase().endsWith('.sune')){\n try{\n const d=JSON.parse(content);\n const html=d?.[0]?.settings?.html;\n if(typeof html==='string'){\n const fence=getFence(html);\n fileContentMd=`${fence}html\\n${html}\\n${fence}`;\n }\n }catch(e){}\n }\n\n if(!fileContentMd){\n const fence=getFence(content);\n fileContentMd=`${fence}${getLang(path)}\\n${content}\\n${fence}`;\n }\n\n return{\n ok:true,\n md:`${h}\\n\\n${fileContentMd}`,\n path\n };\n }catch(error){\n return{\n ok:false,\n md:`${h}\\n\\n*Error posting file: ${error.message||error}*`,\n path\n };\n }\n });\n\n const concurrency=6;\n\n const results=await runPool(\n jobs,\n concurrency,\n (index)=>{\n completed++;\n if(completed<=total){\n const curPath=targets[index]?.path||'';\n setRowStatus(\n i,\n `Fetching ${completed}/${total}: ${curPath}`,\n 'info'\n );\n }\n }\n );\n\n const okMessages=[];\n const errorMessages=[];\n\n for(let idx=0;idx<results.length;idx++){\n const r=results[idx];\n if(!r||!r.md)continue;\n if(r.ok){\n okMessages.push(r.md);\n posted++;\n }else{\n errorMessages.push(r.md);\n }\n }\n\n if(okMessages.length){\n setRowStatus(i,'Posting fetched files...','info');\n await post(okMessages);\n }\n\n if(errorMessages.length){\n await post(errorMessages);\n }\n\n return{\n s:posted,\n t:total,\n skipped\n };\n };\n\n const urls=load();\n const ignores=loadIgnores();\n\n const esc=s=>String(s)\n .replace(/&/g,'&')\n .replace(/</g,'<')\n .replace(/\"/g,'"');\n\n const rowTpl=(i,v='')=>{\n const ig=(ignores[i]||'').trim();\n const preview=ig\n ?'Ignore: '+ig.replace(/\\n/g,'; ')\n :'';\n return`\n <div>\n <div class=\"flex items-stretch gap-1\">\n <input data-role=\"url\" data-index=\"${i}\" type=\"text\"\n placeholder=\"user/repo or user/repo/file or full URL...\"\n class=\"flex-1 min-w-0 rounded-md border-0 bg-white shadow-inner px-2.5 py-1.5 text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 transition\"\n value=\"${esc(v)}\">\n <button data-role=\"copy\" data-index=\"${i}\" type=\"button\"\n class=\"shrink-0 rounded-md w-8 bg-slate-200/80 text-slate-600 hover:bg-white active:shadow-inner active:scale-95 flex items-center justify-center transition-all\"\n title=\"Copy URL\">\n <i data-lucide=\"copy\" class=\"h-4 w-4\"></i>\n </button>\n <button data-role=\"ignore\" data-index=\"${i}\" type=\"button\"\n class=\"shrink-0 rounded-md w-8 bg-slate-200/80 text-slate-600 hover:bg-white active:shadow-inner active:scale-95 flex items-center justify-center transition-all\"\n title=\"Edit ignore patterns\">\n <i data-lucide=\"filter\" class=\"h-4 w-4\"></i>\n </button>\n <button data-role=\"fetch-one\" data-index=\"${i}\" type=\"button\"\n class=\"shrink-0 rounded-md px-2.5 text-xs font-semibold bg-slate-800 text-white hover:bg-slate-700 active:scale-95 transition-all\">\n Fetch\n </button>\n <button data-role=\"remove\" data-index=\"${i}\" type=\"button\"\n class=\"shrink-0 rounded-md w-8 text-slate-500 bg-slate-200/80 hover:bg-red-100 hover:text-red-600 active:scale-95 flex items-center justify-center transition-all\"\n title=\"Remove\">\n <i data-lucide=\"trash-2\" class=\"h-4 w-4\"></i>\n </button>\n </div>\n <div data-role=\"ignore-preview\" data-index=\"${i}\"\n class=\"mt-0.5 px-1 text-[10px] text-slate-400 min-h-[0.75em] truncate\">${esc(preview)}</div>\n <div data-role=\"row-status\" data-index=\"${i}\"\n data-base-class=\"mt-0.5 px-1 text-[11px] min-h-[1em]\"\n class=\"mt-0.5 px-1 text-[11px] text-slate-500 min-h-[1em]\"></div>\n </div>\n `;\n };\n\n const render=()=>{\n const list=urls.length\n ?urls\n :[''];\n els.rows.innerHTML=list\n .map((u,i)=>rowTpl(i,u))\n .join('');\n icons();\n };\n\n async function fetchRow(i){\n const inp=$(\n `[data-role=\"url\"][data-index=\"${i}\"]`\n );\n const btn=$(\n `[data-role=\"fetch-one\"][data-index=\"${i}\"]`\n );\n if(!inp||!btn)return!1;\n\n setRowStatus(i,'');\n\n const url=(inp.value||'').trim();\n if(!url){\n setRowStatus(\n i,\n 'Please enter a path or URL.',\n 'error'\n );\n return!1;\n }\n\n const prev=btn.innerHTML;\n btn.innerHTML=spinner('h-4 w-4');\n btn.disabled=!0;\n inp.disabled=!0;\n\n try{\n setRowStatus(i,'Parsing...','info');\n const parsed=await parse(url);\n if(!parsed)throw new Error('Invalid URL or repo not found.');\n\n if(parsed.type==='file'){\n const result=await fetchFile(parsed.p);\n await post(result.md);\n setRowStatus(i,'Added to chat.','ok');\n }else{\n const ignoreStr=ignores[i]||'';\n const{\n s,\n t,\n skipped\n }=await fetchRepo(parsed.p,i,ignoreStr);\n\n const ignoredInfo=skipped\n ?`, ignored ${skipped}`\n :'';\n\n if(s===t&&t>0&&!skipped){\n setRowStatus(\n i,\n `Added all ${t} files.`,\n 'ok'\n );\n }else{\n setRowStatus(\n i,\n `Added ${s}/${t} files${ignoredInfo}.`,\n 'info'\n );\n }\n }\n\n return!0;\n }catch(err){\n setRowStatus(\n i,\n err.message||String(err),\n 'error'\n );\n return!1;\n }finally{\n btn.innerHTML=prev;\n btn.disabled=!1;\n inp.disabled=!1;\n }\n }\n\n els.rows.addEventListener('click',async e=>{\n const b=e.target.closest('button[data-role]');\n if(!b)return;\n\n const i=+b.dataset.index;\n\n const actions={\n copy:async()=>{\n const n=$(\n `input[data-index=\"${i}\"]`\n );\n if(!n?.value){\n setRowStatus(i,'Nothing to copy.');\n return;\n }\n try{\n await navigator.clipboard.writeText(n.value);\n setRowStatus(i,'Copied!','ok');\n }catch(e){\n setRowStatus(i,'Copy failed.','error');\n }\n },\n ignore:()=>{\n const cur=(ignores[i]||'')\n .replace(/\\n/g,', ');\n const hint='Patterns like .gitignore. Examples: node_modules/,dist/,*.log';\n const next=prompt(hint,cur);\n if(next===null)return;\n\n const normalized=(next||'')\n .split(/[,;]+/)\n .map(s=>s.trim())\n .filter(Boolean)\n .join('\\n');\n\n ignores[i]=normalized;\n while(ignores.length<urls.length){\n ignores.push('');\n }\n saveIgnores(ignores);\n\n const pv=$(\n `[data-role=\"ignore-preview\"][data-index=\"${i}\"]`\n );\n if(pv){\n pv.textContent=normalized\n ?'Ignore: '+normalized.replace(/\\n/g,'; ')\n :'';\n }\n },\n 'fetch-one':()=>fetchRow(i),\n remove:()=>{\n if(urls.length>1){\n urls.splice(i,1);\n ignores.splice(i,1);\n }else{\n urls[0]='';\n ignores[0]='';\n }\n save(urls);\n saveIgnores(ignores);\n render();\n }\n };\n\n const fn=actions[b.dataset.role];\n if(fn)fn();\n });\n\n els.rows.addEventListener('input',e=>{\n const n=e.target.closest('input[data-role=\"url\"]');\n if(!n)return;\n urls[+n.dataset.index]=n.value;\n save(urls);\n });\n\n els.rows.addEventListener('keydown',e=>{\n if(e.key!=='Enter')return;\n if(!e.target.matches('input[data-role=\"url\"]'))return;\n e.preventDefault();\n fetchRow(+e.target.dataset.index);\n });\n\n els.add.addEventListener('click',()=>{\n urls.push('');\n ignores.push('');\n save(urls);\n saveIgnores(ignores);\n render();\n const inp=$(\n `input[data-index=\"${urls.length-1}\"]`\n );\n if(inp)inp.focus();\n });\n\n els.all.addEventListener('click',async()=>{\n const list=urls\n .map((u,i)=>({u:u.trim(),i}))\n .filter(x=>x.u);\n\n if(!list.length){\n setGlobalStatus(\n 'No URLs to fetch. Add one with +.',\n 'error'\n );\n return;\n }\n\n const prev=els.all.innerHTML;\n els.all.innerHTML=spinner('h-4 w-4');\n els.all.disabled=!0;\n\n setGlobalStatus(\n `Fetching ${list.length} item(s)...`,\n 'info'\n );\n\n let s=0;\n let f=0;\n\n for(const{ i }of list){\n (await fetchRow(i))\n ?s++\n :f++;\n }\n\n els.all.innerHTML=prev;\n els.all.disabled=!1;\n\n if(!f){\n setGlobalStatus(\n `Fetched ${s} item(s) successfully.`,\n 'ok'\n );\n }else{\n setGlobalStatus(\n `Completed: ${s} succeeded, ${f} failed.`,\n 'error'\n );\n }\n });\n\n els.status.dataset.baseClass='mt-1.5 px-1 text-[11px] min-h-[1em]';\n\n render();\n})();\n</script>\n","extension_html":"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>","hide_composer":false,"include_thoughts":false,"json_output":false,"ignore_master_prompt":false,"json_schema":"","presence_penalty":0,"max_tokens":0},"storage":{}}] |