Files
store/catalog.json
2025-09-03 22:07:04 +00:00

414 lines
167 KiB
JSON

[
{
"name": "SuneBuilder.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/SuneBuilder.sune",
"sune": [
{
"id": "bklh46m",
"name": "Sune Builder Girl",
"pinned": true,
"avatar": "data:image/webp;base64,UklGRswXAABXRUJQVlA4WAoAAAAgAAAAXwAAjwAASUNDUMgBAAAAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADZWUDgg3hUAABBJAJ0BKmAAkAA+TRyKRCKhoRs9dnwoBMSzAF+5Bf9y6Y7snnPNKrX+x/r+9knL7iM8P+79TvmCc8TzFebT6Nv7Z6h37M9ap6Bvl1+zb/h/+96UvX/6eDwZ/B/kV5v+KP0F7X+s1/XeGLobzI/kv3j/Kfrj61f7Lwf+Pn9L6gX5N/Qf8x+Z/vJ/Mf8Dt29t/0voBezH1X/Uf3/9w/8R6GP+Z6G/Vz/re4B/Mf5//vv7R+7Hwf/p/+F4m/23/Tf8v/I/sB9AP8r/sn+9/wv7if8L4/f9r/Pfl37U/0X/H/9n/Sfk79gv8r/qn+6/vv+W/+H+q///1s+v/9y/ZI/Xb/rMM3hqZYV40B8iceQ8GZ3YzpW1J0K8LXQWti2c/wasB1lkMcnDEpHkQFjLQeDAJhvgI2ESsO2amUB+KXATat/yOuwKnyBIXj8B2znkcx8BVTAn9ujw5aWq1+T1wAXQ0Y+iAYzvm9S/tga2Ue5at2XxMG8VcLIB9JssQY0y2OfySIqkbsIXfj42n7vqyIRDl1W5zNqNSqJlm7CGQotrTKIrAxqbodP64p7Fil1fbSUDl3mCb9QipCcfINUmdy0oeje2AuaUmY2nprYy/s+BEqQbAYhArcQ/E2aaWP/5lC9V3FIxztsqBqKcSLxLlxLPpgnf7khO8S6CZlz0xlcjNvyJmME6kLev2Xw3HyD2GH1X4I8uAMMYXNXMv3zJOmlgSnl1h83kfwDkTJcJ2k2OSpbgKgYAsDt7Li1nzHDSHtcIp+CmYIh6nVfnhUyRn4Xv/bEAAP7/1RgfJ1eawy5FzKikAFpv3395OWrsrRvky0QATGtz7I4eslI/S1s2uD2Tmgqrh1dh4Ltwsca7XsaNyf4p8aawIzANoLHlAsyhRkhREwfNxLX4ArPeHXFUxllatv1N2Jzma3jcmd3L2wPARJyKQfJ5UHmGV7sbLX/vnNmkRY1LCEmipwtskB809LctKhjDDdB6V9fqjSQT6fWAY2pOtPvpV8WzdPfPotEYxrUM5vUtkGcT9IABx6veSqWEKVKpEaC9OfRzRWFK11/0xvhU66Ym98MnsTFvDAMZk8pwLswJGm/MHl13zJoizv2Llpq/495q3nDAbXGanuuRnHB3LOOH25YAWlKsaiyfPwCgJJv9kH8Jm2DiWR4RfuaZIB8+eWkxt3rQGUOYiE71UcM+VN2r9o60UgZG1bh+6y6Vuri+zxWhKF/cys2R0E1E00ZJ+PJAqp1Td/RO0dsl4P3+K6Y9PLHqJBjNI3v4gYMReuAlY0i33taJLDpoJTPAVqACoIdfiGQznJl/UFpwOiiaHI6ChSz/Fb2G48vNDg5GauDpe3ftz7jDHEsTttzm2FLP8y544XPCN4DKjMiyX/1AkhQ1xTYaOVYs4eKD8uYVRg3Koq8QipfB1ZpOAoccvCvEkq1h4m2NYlgxV8kMOVuNfcgIhsAALPomiQnFb8I2YGqj8EuO5PBMR1gyCNGVlQvrbVkUBWYNWi7/GW1+jiW7z8HwmLEEJq0qGAOcUVarklppNrNyHQvSSpAHdRm6NnRZHu4OsDFM4Gt6b8tvP0hg2QKFUpHucnyIdBeLjSM85kTuNEGnuPz6obpojP9BTp05WrtQjujBMgGCI/G5E/fzcNSBXkx7TGrlAIW6cLKPeEu/Z9ysVTSlGGXR8PRZN24EcEPHuQl/1YkveUdKhIqmHW2fNnPOTAo/G/qkUCCmAH6DtVZyAp28oZ3fqCn+vubrkH5TfFaBb9awSXwBnzwiYtSH+YBkOJjGTvuWifrwnaCD1LonFyWWf5oLahrivgpqymQHl6npH8093EF8tn99AzQy1BQ6jKS3spJMWfNNEvxQNQ3rMbMQHgPyP/P8of7Wu041C1p0W2y6yfYZp3a6O4Tw+zx9NvBs7i7A4h3wCzsTGRZP8vLl+MmTAf9zhCKQc5s4MZhExpSyMVv9Bwo10Kdj3+EMh/Z/PhGnYv1tLMRwnFfnBBI5VrBvfHnTdYjSilbCt9TfyKRgyx8FxCCnvYyZosSb0msVzNzo9+gV7mJy38rKqJmudHpiIKiIWRVAt5WkUNxORgEK21AvJsW0aXhyT1nUo29WCZvg34t7prj+QX4Px2+iMB1WNr3/2GZs8hQzs8rqs5lRo5n/Lp1lgwmZYgLUFC2tQWQbtT4eSBswtun2hmiOzjgaKzqXvAHOt9TQDQHdQ5fCATpvmRLCcXy459BuofbFxhJieESpfypgIMBcYY7CwHi79RhQK1ChjCITN0KnBBo1v3UcrsCnnmOKWWdB6Faz2AzlyKaKSINT14HMkix2aqG3xgywT9pzwI1rNiBGpT+kMYya7aCt5o7w/Kl/2S2pIUa9fDnTXZUuSlrXwbV4/WBo4a7qpmB1Zsxjs2utK2lvMD+MFY0whXLqaC1MXSRIHBoL/nRD5lJZgRlBPGoojcc6XXyifTdDn+pem/YZ3PJPOQz/xq2YPwqVj3hngA+8U5S1QUfA23MzoMmm6MQIUmLDAyrjL36PCde1Xz6M2PNtdcBpCoAE9L6uUfF4CFM2BiUfyDwtblOBVQrXyd7gv/MybyuWd6HfKMFx0MimU7C18LtSLrLPOEl0poz8Q14slHD9LuOBr/NC+SlIZGLwFd9m/zYQqKwc9qlZGdh+ZFEMbZcr5jbjYj9d2pjf+VYXl4Rg6NMzqa/QDODXUZm1eqqOOm1XLLT4s08YX+hibdeeVox/Oysd9e6DM3UOcMz32NU5NRD1bUocuNQ7RERP+QJuxALNgfjhWEkki3ijCnT0T+t/6ywk31PPF6UFea8CGGlyxzSqDyyKn6wVWRi/pHKrlpxmsemz3uj0VCGUKq8ydvglB7g7wIxipMUAmgE6qGpDKhv492ytbdltXETeU+kVF9m8N1W2fKBDYrohlRejrfnY6WAseX1xF85bwRjNUkLrDpqaQOBmU3rkK8CXg7QS01JXB0pJWVgTTF3LMN3QRSu0DIOpvl12VKXuiD+Lg0VvQ3xMgm4InuXiaTkzM22i+E6G82H5/9p+fVI9vpy+rFBoE22xfzWy6cz5jAmE7Yqi7a3k0rqKoN0zH+/gBbPabW2Ddb3rgdGtt3q5a+5SHoWkgV6gfBKpCt4P8w00kGRct/A2FlotQaEpaUMFjrpDlcvOA6w522nuhgBoqHyQHTYrvS26GHYEDhQ64Wjh0+ABld4Urprvn6duqjE3mYzb5hTvcY66q3zy2fgD5S4iMGX9/QUEDCiz+4UzViNnksJKtx6N4YOsK54s0SqL4prts77L4pYJ1fq0vrdc+wmh+7TgKdd4QgIcthPlgVIFDhDEF0iGctvoJGeVJj3nQU0Cv6yxyRf12LpQOngcirKONYiIgJueGVrryiOdDU8pa9iJJBAwoQ83wjN0x2XxS8joQgduucCAH48wcaVFhl8SyMIV7Q+eYA8buXoAGV6rO8+9uB+IHWNWwLnEvJP9uk9nuOmvp2oLsB116hMd8OuBsszEdBsTzrco8c8iEWJVK44Zi3RVL2IaJZb1EXF1dLwgVKrspQIMfgm5ZAOhxXn7YqBSxboBVONed9mn60++jz8hfr7IaGV+WbB6CemxtJ0GS6vX21z6mMAnd8MQDEQ0XIRz8+9X5rK2qA7tfx0i/Bm/JCP8rnXgoy8xtA0+k0sOLWhrHN6zz8RUPw2Arq3AAYYNUddEXn6aXHbu4U7hWqqawCmrexv1Li/u2z0LBo/CQS482z9yTV97Hap6MrCMIJomm0ReGcip7tsuH/6ut+3X1PCIOoDz9NtjmQIODrq51ewVFf94XcT7Jcd7DBG4KTn+x4981fuYlSWoSFhmIgqKZCtc4nbKReNQrm9OaVFJXMwqCoELKKzD6csonte0ggNfQhdRFrFWNTns7i09P7sFmsHRILdbbca7AqucWltKplB8odDmEz3v55TpJNSrX+OdqD/wKpacHgwZKVc/SLaZOLRLoAyBO6Cd+HpQ7EvgAomASIkfxZUjmKeqlm5nhaxSWswsABc/pm2LfBnmWtW1LdoZn5rh+7jJQ8bPbtCEA/qV4t+xrT6VGm6vFezmvXY9eEq9vsusE0U2cZSNCZxm2qotsvln7+MHhYcmB4ylY4LVk9zfXVk1kkMBe9+FKndrLd8UtDnY5bawZf1xok4al9BK5Bqk6D55kEkwSv91JdzUCIfuITW+626rKMRrGy9UjEPq6Z/0QhzFsAW0XSQUScJIysdYi0gE6IsoStk8qyHNTpyfTQnXfraQyqzQ74Qm/Z1VfTZ51dNFXzs2z1EJ5x3uZh7zf77P6Ro/0eiP0C28LbWJQ5TRWxsLc8yc/lb/MDfnfjoq6XoSleWC2NiUCKH/42Yz8+LXbT5EA0Cyhxgj/tWynLTtA67VGr6+pSIOL8PHTSSikhRiY15jReUad2E5yzwdroxOBL9qt85oN4hhQbICXAX2X354psltnZPBt5W7T5yPMQ/hNNbBcsunwWyZQwbRslWB+TThh0zaAt+7f4fgZB3MhIAmwHBn1qC2HGNMTrNS3F0vm+zWALG0r52vElMiiU8YVWQzJsrV6pO9iulwoX5pIONWI4VpF45zpjD+ZwV386lo3/m7FCs8w8AO9GB8yyYTwCGZTtav8oIuYRP3/eSvhYtwGTnBRXzWFZJwW87soBjAVUJaDfSJqFuppDJzt1llMYS1nEdMmZYyHIoj6hCS/aGk5iu2Ap/uLHaYfwfuVGWVz9XM7OcA3oOzj+DikT8+UKNnt+TZSHXe60aJcWsQmN9i4o/xrBP9YCQYZiq5dk9Oym5nrxNPQtqkN3+JG0h2EEJNWGXbVISKucZjH8U9coETobbNn/kv8TIxqjOqUR91or34M9nRvWC6JfpyaiIQQcwjQ/f0X9R9UtmAbrpTF8eJcmfvJZ+dWX1em/DIlQLgCLAn89BXo2KZ++8wcrGDfwwjSs9JbAJDIIenrw7sAFbpzpNkt9S9lUgC4S7SzjKrMatqQQGpRSe8YIIT59wwGxp6C4taOthwY74r+ulgjIQ4e2Fgo83BCHLTnW6i8w/qDQx/Bex1yK+b8nYfoyv86xR2NXgyJwxntztnMUks+l9NTIijj0ehaaR2d63Rt0t2nL7vjq9Zp9VzPRQ9t515vQ2VHlFsl/RX9WaMIkY5wyF9P9ZqXSAStRZWYGlEy5MVvQVHlGjq15s7O6W5a03h1cwrg9IKHeux+knFcfZ+oe6+mL+Khnp8PK8rcx1mWKnlClCU13N7H7f3j5shll1H8PxYhkX2Oh69LFfJ/4w6zYTfawhGicR+ZotPxLjhC7fTOv79YfjCrsLeyioZuv9SgByuuLgXmZ+r2zXppWQJYkckBmR8aU8we6JN9udHmgCWTNbLKVbGGTU8gKd1RBsyMNlB1h/GIMXTasiSrH+HlgdAN10a+ew3gaafEo1KjM/LAyGEpOtKR+ts+CXjdre6MUyLqMdbfd+4CzLJThZXnjeE9pm4PAG6oWbGCgNio6rSHqheT3r8h91BETWkQ8cM93j2XTSYo6HFciPA7HU5MMU/+Xp7pLczJs+VqA2onD+r3nDTYkkuZy4YTDlyyZTvRvh0+ITp7Fk9VdLY74w0ze86n6j54z9b/HJDwUbt25BzXD8rVK4efMIcZ5oKPOZaAe1Dk5bISG9zeEYwLuJpLqGnhI4DibyXM2jczl8aHK/yP6BpxNMHm/OOMf2CSAqHCNgm3PTDTi/lvO2ULZrE5cQO0a+7uNOgzd3G7BXdZn97Sd0jb5JZC3JQl9PrehETJd760rRWglv4in/lB8/l6vFXaPzYRUFzHoo2Aui0Su16N5mqWDJDGu1v7W+hFeZ7y9qlHdVF7QeRhJSz/bFQag+v2YN+PkcdtykMbT0K5i2VCRBXX4WbtwKUCSRED9tnKL/dd0jlXnkkdYWB2nlSiQ+RBKGuNf2ljYR/k5EEr4h2xCb7a1fgz6lV5zuHX5rDFiVbzU3CY19ntAtO0rZR9HFW5dU0KUSr/UGYh9yfb8v5ghFdazM8Myku+QsUFHGZeBXsMsbW+jBDlyOgQ8yjSjHl+iy87HSVCYDYs+1x1KiUN3mAm22lC/tq1rKg96rmO9Ag0snTLW78N7g40z14+jt7xmdIZuUy6x65abzzRoYShtGh1WrlsJg42qB0iBLDs6vE+4/aYaZjQclgYgFuGF/LbvnBjszabx06pmTljgSFKVA8Fwtmmuv31Y29/i8uv6LUtxgt6mTxBNarqAVAMrLwBIhmSxQSIkeaTczyzhkXcNyIaT0v2SDuzbnFgL/0+0N2ANcgGwQco2qW/LC9vAOZP+YH+eOZ3cmKMO3elz5YeLHSt5D93atNgKvHvi1HeT0i9quWBLJKBDXC+7FdbpWGIKsTUNg0pnx7AU9AdSR/Xuz+pcF83FeXNfiELHdFq1sNQmnkFnobywmAP77ic6/4mAFjDZqKmnq2TFaA7KIINQw/29M1NUgoet1qhOaTV07nxrNla1AcTTJk47uczwgBVo2H1nACdeLVZ/VonTNtpbrDSEneNRucKSayCMLW+zg7OakjKGcJuMZNmCvEokCagmj2C+wt/cC2u6XBXm8k6q3BYa07apwHaSv4vcEr7zn9tULIb2DGMJk66MF+gWQVdeUdIJeS5GR8RFAfUCjXEo7YQWvXvS/9xeL2oJtkINwylMrTDkAWT5BmN/RQSgWcxR/LOQfLX1h15X2XP6JriT4ct+bjLAUydr8wlmDLfBuqqgs0kchJQKikC8XJNKNyPcz2+dEF6b9b7FMx47s1bLggWGVE5ManMFfy2K/QL2rFvcIzJlBl/MQcQsrmNHFF4damcINZ1o9IgzuRXBEFAfWYElz5OY3lG3w2cu5aD7+jwb9Vl/AeQ311G9Jpks0DLR6X/SBjLCREOhlhnBGD+slNynAaKQBFXRYxtq2Z+2YAC2e+QM6X9+6vpU79LfHitfGi2SzYbzXZ6rhLBMKZUMlN/wcJ65AyE/xpBTR9Sv64XgPM9ZeTcfO2dII6WfCInmlGBO7M5UFNlJMMzEQ40aEX86tDP+VGdlbUq4l+/xpMbHFuEeUy41DpNFj6ofMjrUVImHJK/qWfjbCfVNfsiB4uwfmeSIxxp+WiPpJjf8DauBiqajip1bhWx9Equ/v5AZS6E8FP/lzvEhZ86WRSYJI39x+tqFa7Ib5rGBHqWlEqFG7TyMscB/iw9XoKzDikMqGmrdbqG/6ToZKDX0CZqahuiSQLUE9j2BRbuaDKO+4bCn/gogGjYpnfhE0YncO+CqcS7vwXPgEH7zvZttGLXKlvIc8cTUWd9/yG1Pms9QZ9wDzOe4mjsDz36EFdueMSJvcnuWuNh3avn4vZJqAQPPHLxG4hv1ZA82Y82AxxERDCPtUPpERHSSpqEsYBLUKvQVx6Rvb+mMYkYb9JNWGPewcn9XleteuLpP+w9i8amOWnlhlEwzUXnHibCAjx9Ci5xPl7CiIAAA==",
"url": "gh://sune-org/store@main/SuneBuilder.sune",
"updatedAt": 1756931304860,
"settings": {
"model": "or:google/gemini-2.5-pro",
"temperature": 1,
"top_p": 0.97,
"top_k": 0,
"frequency_penalty": 0,
"presence_penalty": 0,
"repetition_penalty": 1,
"min_p": 0,
"top_a": 0,
"max_tokens": 0,
"verbosity": "high",
"reasoning_effort": "default",
"system_prompt": "You are SuneBuilderGPT. You deliver production-ready code. A sune is basically a modular piece of HTML that gets injected into already existing HTML. it can contain scripts and use tailwind. Assume tailwind CDN and lucide CDN already exists. UI should be mobile-first. Utilize web workers (if necessary). Utilize localstorage to cache inputs; use window.SUNE.id as the key. Respond with the HTML. Include a version number in the UI.",
"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, '&quot;')}\"\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>\n\n",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/titles.sune' private></sune>\n\n<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",
"script": ""
}
}
]
},
{
"name": "action_runner.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/action_runner.sune",
"sune": [
{
"id": "hmhdnfo",
"name": "Action Runner",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@main/action_runner.sune",
"updatedAt": 1756931215063,
"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 Actions Runner -->\n<div id=\"ghActionsRunner\" class=\"p-4 space-y-4 text-sm bg-white rounded-lg\">\n\n <div class=\"prose prose-sm max-w-none\">\n <h4>GitHub Actions Runner</h4>\n <p>Enter a repository to trigger workflows. Requires a GitHub token with <code>repo</code> scope, set in <button id=\"ghActionsOpenSettingsBtn\" class=\"text-blue-600 hover:underline\">Account Settings</button>.</p>\n </div>\n\n <div class=\"flex flex-col sm:flex-row items-stretch gap-2\">\n <div class=\"relative flex-grow\">\n <i data-lucide=\"github\" class=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400\"></i>\n <input type=\"text\" id=\"ghRepoPath\" placeholder=\"owner/repo\" class=\"w-full pl-9 pr-3 py-2 bg-white border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:outline-none transition\">\n </div>\n <button id=\"fetchWorkflowsBtn\" class=\"px-4 py-2 bg-black text-white rounded-xl hover:bg-black/90 active:scale-[.98] transition flex items-center justify-center gap-2 disabled:bg-gray-500\">\n <span id=\"fetchBtnText\">Fetch Workflows</span>\n <div id=\"fetchSpinner\" class=\"hidden animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full\"></div>\n </button>\n </div>\n\n <div id=\"ghStatusMessage\" class=\"text-gray-600 min-h-[1.25rem]\"></div>\n <div id=\"workflowsContainer\" class=\"grid grid-cols-1 md:grid-cols-2 gap-3\" data-default-branch=\"\"></div>\n\n</div>\n\n<script>\n(function() {\n // Ensure the sune runs in its own scope to prevent conflicts.\n \"use strict\";\n\n const suneId = window.SUNE ? window.SUNE.id : 'gh_actions_default';\n const container = document.getElementById('ghActionsRunner');\n if (!container) return;\n \n // Check for core dependencies from the main application.\n if (typeof globalStore === 'undefined' || typeof openAccountSettings === 'undefined') {\n container.innerHTML = '<p class=\"text-red-600\">Error: Core dependencies `globalStore` or `openAccountSettings` are missing. This sune cannot function.</p>';\n return;\n }\n\n const el = {\n repoInput: document.getElementById('ghRepoPath'),\n fetchBtn: document.getElementById('fetchWorkflowsBtn'),\n fetchBtnText: document.getElementById('fetchBtnText'),\n fetchSpinner: document.getElementById('fetchSpinner'),\n statusMsg: document.getElementById('ghStatusMessage'),\n workflowsContainer: document.getElementById('workflowsContainer'),\n openSettingsBtn: document.getElementById('ghActionsOpenSettingsBtn'),\n };\n\n // Use the unique SUNE ID for localStorage to avoid conflicts between different sune instances.\n const getStorageKey = () => `sune_gh_actions_repo_${suneId}`;\n\n const getGhToken = () => globalStore.ghToken || '';\n\n /**\n * A wrapper for the GitHub API fetch calls.\n * @param {string} url - The API endpoint path (e.g., /repos/owner/repo).\n * @param {string} token - The GitHub API token.\n * @param {object} options - Standard fetch options.\n * @returns {Promise<Response>}\n */\n const apiFetch = (url, token, options = {}) => {\n const headers = {\n 'Authorization': `Bearer ${token}`,\n 'Accept': 'application/vnd.github.v3+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n ...options.headers,\n };\n return fetch(`https://api.github.com${url}`, { ...options, headers });\n };\n\n /**\n * Toggles the loading state of the fetch button.\n * @param {boolean} isLoading - Whether to show the loading state.\n */\n const showLoading = (isLoading) => {\n el.fetchSpinner.classList.toggle('hidden', !isLoading);\n el.fetchBtnText.textContent = isLoading ? 'Fetching...' : 'Fetch Workflows';\n el.fetchBtn.disabled = isLoading;\n };\n\n /**\n * Displays a status message to the user.\n * @param {string} message - The message to display.\n * @param {boolean} isError - If true, formats the message as an error.\n */\n const setStatus = (message, isError = false) => {\n el.statusMsg.innerHTML = message;\n el.statusMsg.className = isError ? 'text-red-600' : 'text-gray-600';\n };\n\n /**\n * Renders the list of runnable workflows.\n * @param {Array} workflows - The array of workflow objects from the GitHub API.\n * @param {string} repoPath - The \"owner/repo\" string.\n */\n const renderWorkflows = (workflows, repoPath) => {\n el.workflowsContainer.innerHTML = '';\n const activeWorkflows = workflows.filter(wf => wf.state === 'active');\n if (activeWorkflows.length === 0) {\n return setStatus('No runnable workflows found in this repository.');\n }\n\n activeWorkflows.forEach(wf => {\n const btn = document.createElement('button');\n btn.className = 'w-full text-left p-3 border border-gray-200 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition flex items-center justify-between gap-2';\n btn.innerHTML = `<span class=\"font-medium truncate\">${wf.name}</span><span class=\"flex items-center gap-1.5 text-xs text-gray-500\"><span>Run</span><i data-lucide=\"play\" class=\"h-4 w-4\"></i></span>`;\n Object.assign(btn.dataset, { workflowId: wf.id, workflowPath: wf.path, repoPath });\n el.workflowsContainer.appendChild(btn);\n });\n\n if (window.lucide) window.lucide.createIcons();\n };\n\n /**\n * Fetches the repository details and its workflows.\n */\n const fetchWorkflows = async () => {\n const token = getGhToken();\n if (!token) {\n return setStatus('GitHub token not found. Please set it in Account Settings.', true);\n }\n\n const repoPath = el.repoInput.value.trim();\n if (!repoPath.includes('/')) {\n return setStatus('Invalid repository format. Use \"owner/repo\".', true);\n }\n\n showLoading(true);\n setStatus('Fetching repository details...');\n el.workflowsContainer.innerHTML = '';\n\n try {\n const [repoRes, wfRes] = await Promise.all([\n apiFetch(`/repos/${repoPath}`, token),\n apiFetch(`/repos/${repoPath}/actions/workflows`, token)\n ]);\n\n if (!repoRes.ok) throw new Error(repoRes.status === 404 ? 'Repository not found or access denied.' : `Repo fetch error: ${repoRes.statusText}`);\n if (!wfRes.ok) throw new Error(`Workflow fetch error: ${wfRes.statusText}`);\n \n const [repoData, wfData] = await Promise.all([repoRes.json(), wfRes.json()]);\n el.workflowsContainer.dataset.defaultBranch = repoData.default_branch || 'main';\n \n setStatus('');\n renderWorkflows(wfData.workflows, repoPath);\n localStorage.setItem(getStorageKey(), repoPath);\n } catch (error) {\n setStatus(error.message, true);\n } finally {\n showLoading(false);\n }\n };\n\n /**\n * Handles the logic for triggering a specific workflow.\n * @param {Event} e - The click event.\n */\n const triggerWorkflow = async (e) => {\n const button = e.target.closest('button[data-workflow-id]');\n if (!button) return;\n\n const token = getGhToken();\n if (!token) return setStatus('GitHub token not found.', true);\n \n const { workflowId, workflowPath, repoPath } = button.dataset;\n const ref = el.workflowsContainer.dataset.defaultBranch;\n const inputs = {};\n\n const originalContent = button.innerHTML;\n button.disabled = true;\n button.innerHTML = `<span class=\"font-medium truncate\">Inspecting...</span><div class=\"animate-spin h-4 w-4 border-2 border-gray-400 border-t-transparent rounded-full\"></div>`;\n setStatus(`Checking for inputs in ${workflowPath}...`);\n\n try {\n const contentRes = await apiFetch(`/repos/${repoPath}/contents/${workflowPath}?ref=${ref}`, token);\n if (!contentRes.ok) throw new Error('Could not read workflow file to check for inputs.');\n const fileContent = atob((await contentRes.json()).content);\n \n // Lightweight regex to find workflow_dispatch inputs without needing a full YAML parser.\n const dispatchMatch = fileContent.match(/on:\\s*([\\s\\S]*?)(?=\\n\\w|\\n$)/);\n if (dispatchMatch && dispatchMatch[0].includes('workflow_dispatch')) {\n const inputsMatch = dispatchMatch[0].match(/inputs:\\s*([\\s\\S]*?)(?=\\n\\s*\\w|^\\w|\\s*$)/);\n if (inputsMatch) {\n // Extract input keys\n const inputKeys = [...inputsMatch[1].matchAll(/^\\s{2,}(\\w+):/gm)].map(m => m[1]);\n for (const key of inputKeys) {\n const val = prompt(`Enter value for input: \"${key}\"`);\n if (val === null) throw new Error('Workflow run cancelled by user.');\n inputs[key] = val;\n }\n }\n }\n\n button.innerHTML = `<span class=\"font-medium truncate\">Triggering...</span><div class=\"animate-spin h-4 w-4 border-2 border-gray-400 border-t-transparent rounded-full\"></div>`;\n setStatus(`Triggering workflow: ${button.querySelector('span').textContent}`);\n\n const dispatchRes = await apiFetch(`/repos/${repoPath}/actions/workflows/${workflowId}/dispatches`, token, {\n method: 'POST',\n body: JSON.stringify({ ref, inputs })\n });\n\n if (dispatchRes.status !== 204) {\n const errorData = await dispatchRes.json().catch(() => ({}));\n throw new Error(errorData.message || `API error: ${dispatchRes.status}`);\n }\n\n setStatus(`Successfully triggered workflow. <a href=\"https://github.com/${repoPath}/actions\" target=\"_blank\" rel=\"noopener\" class=\"text-blue-600 hover:underline\">View runs</a>.`);\n button.innerHTML = `<span class=\"font-medium truncate text-green-600\">Triggered!</span><i data-lucide=\"check-circle\" class=\"h-4 w-4 text-green-600\"></i>`;\n if (window.lucide) window.lucide.createIcons();\n\n } catch (error) {\n setStatus(error.message, true);\n // Only restore the button if it didn't succeed\n button.innerHTML = originalContent; \n } finally {\n // Restore button after a delay to show success/failure state.\n setTimeout(() => {\n if (!button.innerHTML.includes('Triggered!')) button.innerHTML = originalContent;\n button.disabled = false;\n if (window.lucide) window.lucide.createIcons();\n }, 3000);\n }\n };\n\n /**\n * Initializes the sune.\n */\n const init = () => {\n el.repoInput.value = localStorage.getItem(getStorageKey()) || '';\n el.fetchBtn.addEventListener('click', fetchWorkflows);\n el.repoInput.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n fetchWorkflows();\n }\n });\n el.workflowsContainer.addEventListener('click', triggerWorkflow);\n el.openSettingsBtn.addEventListener('click', () => { \n if (window.openAccountSettings) window.openAccountSettings(); \n });\n \n if (window.lucide) window.lucide.createIcons();\n };\n\n // Run the initialization logic.\n init();\n\n})();\n</script>\n",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />"
}
}
]
},
{
"name": "catalog.json",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/catalog.json"
},
{
"name": "commit-history.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/commit-history.sune",
"sune": [
{
"id": "28qvo5s",
"name": "File History",
"pinned": true,
"avatar": "",
"url": "gh://sune-org/store@main/commit-history.sune",
"updatedAt": 1756931360563,
"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": "<div id=\"ghHistorySune\" class=\"p-4 bg-white rounded-lg border border-gray-200 shadow-sm max-w-2xl mx-auto font-sans\">\n \n <!-- Header -->\n <div class=\"flex justify-between items-center mb-4\">\n <h2 class=\"font-bold text-lg text-gray-800\">GitHub File History</h2>\n <span class=\"text-xs text-gray-400\">v-0.1.0</span>\n </div>\n\n <!-- Input Form -->\n <div class=\"flex flex-col sm:flex-row gap-2 mb-4\">\n <div class=\"relative flex-1\">\n <i data-lucide=\"github\" class=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400\"></i>\n <input type=\"text\" id=\"ghPathInput\" placeholder=\"org/repo@branch/file/path\" class=\"w-full pl-9 pr-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors\">\n </div>\n <button id=\"fetchHistoryBtn\" class=\"flex items-center justify-center px-4 py-2 bg-gray-800 text-white rounded-lg hover:bg-gray-900 active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed\">\n <i data-lucide=\"search\" class=\"h-4 w-4\"></i>\n <span class=\"ml-2\">Fetch</span>\n </button>\n </div>\n\n <!-- Status & Results Area -->\n <div id=\"resultsArea\" class=\"space-y-3\">\n <div id=\"statusArea\" class=\"text-sm text-center text-gray-500 p-4 border-2 border-dashed border-gray-200 rounded-lg hidden\"></div>\n <div id=\"commitList\" class=\"space-y-2 max-h-[50vh] overflow-y-auto -mr-2 pr-2\">\n <!-- Commit items will be injected here -->\n </div>\n </div>\n \n <!-- File Content Modal -->\n <div id=\"fileContentModal\" class=\"fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden\">\n <div class=\"bg-white rounded-xl shadow-2xl w-full max-w-4xl flex flex-col max-h-[90vh]\">\n <div class=\"flex justify-between items-center p-3 border-b border-gray-200\">\n <p id=\"modalHeader\" class=\"text-sm font-medium text-gray-700 truncate\">File Content</p>\n <button id=\"closeModalBtn\" class=\"p-1 rounded-full hover:bg-gray-200 active:bg-gray-300 transition-colors\">\n <i data-lucide=\"x\" class=\"h-5 w-5 text-gray-600\"></i>\n </button>\n </div>\n <div class=\"flex-1 overflow-auto relative bg-gray-900\">\n <pre><code id=\"fileContentCode\" class=\"block p-4 text-xs font-mono leading-relaxed hljs\"></code></pre>\n <button id=\"copyContentBtn\" class=\"absolute top-2 right-12 px-3 py-1 text-xs bg-gray-700/80 text-white rounded-md hover:bg-gray-600\">\n Copy\n </button>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n(function() {\n // Ensure sune runs only once and within its container\n const sune = document.getElementById('ghHistorySune');\n if (!sune || sune.dataset.initialized) return;\n sune.dataset.initialized = 'true';\n\n const SUNE_ID = window.SUNE ? window.SUNE.id : 'gh-history-sune';\n const CACHE_KEY = `${SUNE_ID}_gh_path`;\n\n // --- DOM Elements ---\n const pathInput = sune.querySelector('#ghPathInput');\n const fetchBtn = sune.querySelector('#fetchHistoryBtn');\n const statusArea = sune.querySelector('#statusArea');\n const commitList = sune.querySelector('#commitList');\n \n const modal = sune.querySelector('#fileContentModal');\n const modalHeader = sune.querySelector('#modalHeader');\n const modalCode = sune.querySelector('#fileContentCode');\n const copyContentBtn = sune.querySelector('#copyContentBtn');\n const closeModalBtn = sune.querySelector('#closeModalBtn');\n\n // --- Utility Functions ---\n const showStatus = (message, isError = false) => {\n statusArea.innerHTML = message;\n statusArea.classList.toggle('text-red-600', isError);\n statusArea.classList.remove('hidden');\n commitList.innerHTML = '';\n };\n\n const hideStatus = () => statusArea.classList.add('hidden');\n \n const setLoading = (isLoading) => {\n fetchBtn.disabled = isLoading;\n if (isLoading) {\n fetchBtn.innerHTML = `<svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg><span class=\"ml-2\">Fetching...</span>`;\n } else {\n fetchBtn.innerHTML = `<i data-lucide=\"search\" class=\"h-4 w-4\"></i><span class=\"ml-2\">Fetch</span>`;\n window.lucide && window.lucide.createIcons();\n }\n };\n\n const parsePath = (input) => {\n if (!input) return null;\n const match = input.match(/^([a-zA-Z0-9\\-_]+)\\/([a-zA-Z0-9\\-_.]+)(?:@([^\\/]+))?\\/(.+)$/);\n if (!match) return null;\n\n return {\n owner: match[1],\n repo: match[2],\n branch: match[3] || null, // null will use default branch\n path: match[4]\n };\n };\n\n const b64_to_utf8 = (str) => {\n try {\n return decodeURIComponent(atob(str).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));\n } catch (e) {\n console.error(\"Base64 decoding failed\", e);\n return \"Error: Could not decode file content. It might be a binary file.\";\n }\n };\n\n const copyToClipboard = async (element, text) => {\n const originalText = element.textContent;\n try {\n await navigator.clipboard.writeText(text);\n element.textContent = 'Copied!';\n } catch (err) {\n element.textContent = 'Failed!';\n console.error('Failed to copy text: ', err);\n } finally {\n setTimeout(() => { element.textContent = originalText; }, 2000);\n }\n };\n \n // --- API Functions ---\n const ghFetch = async (url) => {\n const headers = { 'Accept': 'application/vnd.github.v3+json' };\n if (window.USER && window.USER.PAT) {\n headers['Authorization'] = `token ${window.USER.PAT}`;\n }\n const response = await fetch(url, { headers });\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({ message: `API Error: ${response.status}` }));\n throw new Error(errorData.message || `API Error: ${response.status}`);\n }\n return response.json();\n };\n\n const fetchHistory = async () => {\n const pathValue = pathInput.value.trim();\n if (!pathValue) {\n showStatus(\"Please enter a GitHub file path.\", true);\n return;\n }\n\n const parsed = parsePath(pathValue);\n if (!parsed) {\n showStatus(\"Invalid format. Use: org/repo@branch/file/path\", true);\n return;\n }\n \n localStorage.setItem(CACHE_KEY, pathValue);\n setLoading(true);\n hideStatus();\n commitList.innerHTML = '';\n\n try {\n const { owner, repo, branch, path } = parsed;\n let url = `https://api.github.com/repos/${owner}/${repo}/commits?path=${encodeURIComponent(path)}`;\n if (branch) {\n url += `&sha=${encodeURIComponent(branch)}`;\n }\n \n const commits = await ghFetch(url);\n \n if (!commits || commits.length === 0) {\n showStatus(\"No commits found for this file path. Check if the path and branch are correct.\", false);\n } else {\n renderCommits(commits, parsed);\n }\n } catch (error) {\n console.error(\"Error fetching history:\", error);\n showStatus(`Error: ${error.message}`, true);\n } finally {\n setLoading(false);\n }\n };\n\n const fetchFileContent = async (btn) => {\n const { owner, repo, path, sha } = btn.dataset;\n if (!owner || !repo || !path || !sha) return;\n\n modalHeader.textContent = `Loading... (${sha.substring(0, 7)})`;\n modalCode.textContent = 'Loading content...';\n modal.classList.remove('hidden');\n\n try {\n const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${sha}`;\n const fileData = await ghFetch(url);\n\n if (fileData.content) {\n const decodedContent = b64_to_utf8(fileData.content);\n modalHeader.textContent = `${path} @ ${sha.substring(0, 7)}`;\n modalCode.textContent = decodedContent;\n if (window.hljs) {\n modalCode.className = 'block p-4 text-xs font-mono leading-relaxed hljs';\n window.hljs.highlightElement(modalCode);\n }\n } else {\n modalHeader.textContent = `Error loading file @ ${sha.substring(0, 7)}`;\n modalCode.textContent = \"Could not retrieve file content. It might be a submodule or a large/binary file.\";\n }\n } catch (error) {\n console.error(\"Error fetching file content:\", error);\n modalHeader.textContent = `Error loading file @ ${sha.substring(0, 7)}`;\n modalCode.textContent = `Error: ${error.message}`;\n }\n };\n\n // --- Render Functions ---\n const renderCommits = (commits, parsedInfo) => {\n const commitHtml = commits.map(item => {\n const commit = item.commit;\n const author = item.author || commit.author;\n const date = new Date(commit.author.date).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' });\n \n return `\n <div class=\"p-3 border border-gray-200 rounded-lg bg-gray-50/50 hover:bg-gray-100 hover:border-gray-300 transition-all\">\n <p class=\"font-medium text-sm text-gray-800 truncate\">${commit.message.split('\\n')[0]}</p>\n <div class=\"flex flex-col sm:flex-row justify-between items-start sm:items-center mt-2 gap-2\">\n <div class=\"flex items-center gap-2 text-xs text-gray-500\">\n ${author && author.avatar_url ? `<img src=\"${author.avatar_url}\" class=\"h-5 w-5 rounded-full\" alt=\"${author.login || ''}\">` : '<div class=\"h-5 w-5 rounded-full bg-gray-300\"></div>'}\n <span class=\"font-medium\">${commit.author.name}</span>\n <span class=\"hidden md:inline\">&bull;</span>\n <span class=\"flex-shrink-0\">${date}</span>\n </div>\n <button \n class=\"view-content-btn self-end sm:self-center flex-shrink-0 px-3 py-1 text-xs bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-200 active:scale-95 transition-all\"\n data-owner=\"${parsedInfo.owner}\"\n data-repo=\"${parsedInfo.repo}\"\n data-path=\"${parsedInfo.path}\"\n data-sha=\"${item.sha}\"\n >\n View File\n </button>\n </div>\n </div>\n `;\n }).join('');\n commitList.innerHTML = commitHtml;\n };\n\n // --- Initialization ---\n const init = () => {\n // Load cached path\n const cachedPath = localStorage.getItem(CACHE_KEY);\n if (cachedPath) {\n pathInput.value = cachedPath;\n }\n\n // Event Listeners\n fetchBtn.addEventListener('click', fetchHistory);\n pathInput.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n fetchHistory();\n }\n });\n\n commitList.addEventListener('click', (e) => {\n const btn = e.target.closest('.view-content-btn');\n if (btn) {\n fetchFileContent(btn);\n }\n });\n\n closeModalBtn.addEventListener('click', () => modal.classList.add('hidden'));\n modal.addEventListener('click', (e) => {\n if (e.target === modal) {\n modal.classList.add('hidden');\n }\n });\n\n copyContentBtn.addEventListener('click', (e) => {\n copyToClipboard(e.target, modalCode.textContent);\n });\n\n // Initial icon render\n window.lucide && window.lucide.createIcons();\n };\n\n // Run after DOM is ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init);\n } else {\n init();\n }\n})();\n</script>\n",
"extension_html": "\n<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune"
}
}
]
},
{
"name": "commit.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/commit.sune",
"sune": [
{
"id": "bxvj8jx",
"name": "Commit",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@main/commit.sune",
"updatedAt": 1756931202747,
"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 File Editor Sune v1.1.0 -->\n<div id=\"gh-editor-sune\" class=\"mx-0 border-b border-gray-200 bg-gray-50 flex flex-col\">\n\n <!-- Toolbar -->\n <div class=\"flex items-center gap-3 px-3 sm:px-4 py-2 border-b border-gray-200 bg-white/80 backdrop-blur-sm shrink-0\">\n <i data-lucide=\"github\" class=\"h-5 w-5 text-gray-500 shrink-0\"></i>\n <input\n type=\"text\"\n id=\"ghPathInput\"\n placeholder=\"owner/repo/path/to/file.ext@branch\"\n class=\"flex-1 w-full bg-transparent border-none focus:ring-0 p-0 text-sm placeholder:text-gray-400\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n <button\n id=\"ghFetchBtn\"\n class=\"inline-flex items-center justify-center gap-1.5 h-8 px-3 shrink-0 rounded-lg bg-gray-100 text-gray-800 shadow-sm hover:bg-gray-200 active:scale-[.98] transition text-sm font-medium\"\n title=\"Fetch File\"\n >\n <i data-lucide=\"refresh-cw\" class=\"h-4 w-4\"></i>\n <span class=\"hidden sm:inline\">Fetch</span>\n </button>\n <button\n id=\"ghCommitBtn\"\n class=\"inline-flex items-center justify-center gap-1.5 h-8 px-3 shrink-0 rounded-lg bg-green-600 text-white shadow-sm hover:bg-green-700 active:scale-[.98] transition text-sm font-medium\"\n title=\"Commit Changes\"\n >\n <i data-lucide=\"check-circle\" class=\"h-4 w-4\"></i>\n <span class=\"hidden sm:inline\">Commit</span>\n </button>\n </div>\n\n <!-- Editor -->\n <div class=\"bg-white flex-1 relative\">\n <pre\n id=\"ghFileEditor\"\n class=\"w-full h-[65vh] p-3 overflow-auto font-mono text-[12px] leading-5 focus:outline-none whitespace-pre\"\n ></pre>\n </div>\n\n <!-- Footer -->\n <div class=\"flex items-center justify-between gap-4 px-3 sm:px-4 py-1 border-t border-gray-200 bg-white/80 backdrop-blur-sm text-xs shrink-0\">\n <div class=\"flex items-center gap-1\">\n <button id=\"ghCopyBtn\" title=\"Copy\" class=\"p-1.5 rounded-md hover:bg-gray-200 text-gray-500 hover:text-gray-800 transition-colors\"><i data-lucide=\"copy\" class=\"h-4 w-4\"></i></button>\n <button id=\"ghCutBtn\" title=\"Cut\" class=\"p-1.5 rounded-md hover:bg-gray-200 text-gray-500 hover:text-gray-800 transition-colors\"><i data-lucide=\"scissors\" class=\"h-4 w-4\"></i></button>\n <button id=\"ghPasteBtn\" title=\"Paste\" class=\"p-1.5 rounded-md hover:bg-gray-200 text-gray-500 hover:text-gray-800 transition-colors\"><i data-lucide=\"clipboard-paste\" class=\"h-4 w-4\"></i></button>\n </div>\n <div class=\"flex items-center gap-3 text-gray-500 truncate\">\n <span id=\"ghLangBadge\" class=\"font-medium\">Plain Text</span>\n <div id=\"ghStatus\" class=\"h-4 truncate\"></div>\n </div>\n <span class=\"text-gray-400\">v1.1.0</span>\n </div>\n\n</div>\n\n<script>\n(async () => {\n // Elements\n const pathInput = document.getElementById('ghPathInput');\n const fetchBtn = document.getElementById('ghFetchBtn');\n const commitBtn = document.getElementById('ghCommitBtn');\n const fileEditorEl = document.getElementById('ghFileEditor');\n const copyBtn = document.getElementById('ghCopyBtn');\n const cutBtn = document.getElementById('ghCutBtn');\n const pasteBtn = document.getElementById('ghPasteBtn');\n const statusEl = document.getElementById('ghStatus');\n const langBadge = document.getElementById('ghLangBadge');\n\n // State\n const ghSuneState = {\n owner: null, repo: null, path: null, branch: null, sha: null,\n jar: null, token: null, lang: null\n };\n \n // Use the active Sune's ID for a unique localStorage key.\n const getLsKey = () => {\n const suneId = window.SUNE?.id;\n return suneId ? `sune_gh_last_path_${suneId}` : 'sune_gh_last_path_fallback';\n };\n\n // Restore last path for this sune\n try {\n const last = localStorage.getItem(getLsKey());\n if (last) pathInput.value = last;\n } catch (e) {\n console.error(\"Sune: Failed to get path from localStorage\", e);\n }\n\n // --- Helper Functions ---\n const setStatus = (message, isError = false) => {\n statusEl.textContent = message || '';\n statusEl.className = `h-4 truncate ${isError ? 'text-red-600' : 'text-gray-500'}`;\n if (!isError && message) {\n setTimeout(() => {\n if (statusEl.textContent === message) statusEl.textContent = '';\n }, 4000);\n }\n };\n\n const formatCharCount = (count) => {\n if (count < 1000) return `${count} chars`;\n const thousands = count / 1000;\n const formatted = thousands.toFixed(2).replace(/\\.00$/, '').replace(/(\\.\\d)0$/, '$1');\n return `${formatted}k chars`;\n };\n \n const setLoading = (btn, isLoading) => {\n btn.disabled = isLoading;\n const icon = btn.querySelector('i[data-lucide]');\n if (!icon) return;\n\n if (isLoading) {\n if (!icon.dataset.originalIcon) icon.dataset.originalIcon = icon.dataset.lucide;\n icon.dataset.lucide = 'loader-circle';\n icon.classList.add('animate-spin');\n } else {\n if (icon.dataset.originalIcon) icon.dataset.lucide = icon.dataset.originalIcon;\n icon.classList.remove('animate-spin');\n }\n if (window.lucide) window.lucide.createIcons();\n };\n \n const parseGhPath = (input) => {\n const match = input.trim().match(/^([a-zA-Z0-9._-]+)\\/([a-zA-Z0-9._-]+)\\/(.+?)@([a-zA-Z0-9._.-]+)$/);\n return match ? { owner: match[1], repo: match[2], path: match[3], branch: match[4] } : null;\n };\n\n const detectLang = (filepath = '') => {\n const p = String(filepath).toLowerCase();\n if (p.endsWith('.html') || p.endsWith('.htm')) return { lang: 'xml', label: 'HTML' };\n if (p.endsWith('.js') || p.endsWith('.mjs') || p.endsWith('.cjs')) return { lang: 'javascript', label: 'JavaScript' };\n if (p.endsWith('.json')) return { lang: 'json', label: 'JSON' };\n if (p.endsWith('.css')) return { lang: 'css', label: 'CSS' };\n if (p.endsWith('.md')) return { lang: 'markdown', label: 'Markdown' };\n return { lang: 'plaintext', label: 'Plain Text' };\n };\n \n const encodeBase64 = (str) => btoa(unescape(encodeURIComponent(str)));\n const decodeBase64 = (b64) => decodeURIComponent(escape(atob(b64)));\n \n // --- Code Editor Setup ---\n const mod = await import('https://medv.io/codejar/codejar.js');\n const CodeJar = mod.CodeJar || mod.default;\n const doHighlight = (editor) => {\n const code = editor.textContent;\n const { lang } = ghSuneState;\n if (window.hljs && lang && window.hljs.getLanguage(lang)) {\n editor.innerHTML = window.hljs.highlight(code, { language: lang, ignoreIllegals: true }).value;\n } else {\n editor.textContent = code;\n }\n };\n ghSuneState.jar = CodeJar(fileEditorEl, doHighlight, { tab: ' ' });\n\n const updateLangBadge = () => {\n const { label } = detectLang(ghSuneState.path || '');\n langBadge.textContent = label;\n };\n\n // --- API Handlers ---\n const handleFetch = async () => {\n setStatus('');\n ghSuneState.token = window.USER?.PAT || '';\n if (!ghSuneState.token) {\n setStatus('Error: GitHub token not found in Account Settings.', true);\n return;\n }\n\n const parsed = parseGhPath(pathInput.value);\n if (!parsed) {\n setStatus('Error: Invalid path. Use: owner/repo/path@branch', true);\n return;\n }\n\n try { localStorage.setItem(getLsKey(), pathInput.value.trim()); } catch (e) { console.error(\"Sune: Failed to save path\", e); }\n\n Object.assign(ghSuneState, parsed);\n ghSuneState.lang = detectLang(ghSuneState.path).lang;\n updateLangBadge();\n\n const apiUrl = `https://api.github.com/repos/${ghSuneState.owner}/${ghSuneState.repo}/contents/${encodeURIComponent(ghSuneState.path).replace(/%2F/g,'/')}?ref=${encodeURIComponent(ghSuneState.branch)}`;\n setStatus('Fetching...');\n setLoading(fetchBtn, true);\n\n try {\n const response = await fetch(apiUrl, { headers: { 'Authorization': `Bearer ${ghSuneState.token}`, 'Accept': 'application/vnd.github.v3+json' } });\n const data = await response.json();\n if (!response.ok) throw new Error(data.message || 'Failed to fetch file.');\n if (data.type !== 'file') throw new Error('Path is not a file.');\n\n ghSuneState.sha = data.sha;\n const decodedContent = decodeBase64(data.content || '');\n ghSuneState.jar.updateCode(decodedContent);\n setStatus(`Loaded ${ghSuneState.path} (${formatCharCount(decodedContent.length)}).`);\n } catch (err) {\n setStatus(`Error: ${err.message}`, true);\n ghSuneState.sha = null;\n } finally {\n setLoading(fetchBtn, false);\n }\n };\n\n const handleCommit = async () => {\n setStatus('');\n const { owner, repo, path, branch, sha, token, jar } = ghSuneState;\n\n if (!token) { setStatus('Error: GitHub token not found.', true); return; }\n if (!owner || !sha) { setStatus('Error: Fetch a file first.', true); return; }\n\n const defaultCommitMsg = `Update ${path.split('/').pop() || path}`;\n const commitMsg = prompt(\"Enter commit message:\", defaultCommitMsg);\n\n if (commitMsg === null) { setStatus('Commit cancelled.'); return; }\n if (!commitMsg.trim()) { setStatus('Error: Commit message cannot be empty.', true); return; }\n\n const newContent = jar.toString();\n const encodedContent = encodeBase64(newContent);\n const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${encodeURIComponent(path).replace(/%2F/g,'/')}`;\n \n setStatus('Committing...');\n setLoading(commitBtn, true);\n\n try {\n const response = await fetch(apiUrl, {\n method: 'PUT',\n headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' },\n body: JSON.stringify({ message: commitMsg.trim(), content: encodedContent, sha, branch }),\n });\n const data = await response.json();\n if (!response.ok) throw new Error(data.message || 'Failed to commit.');\n\n ghSuneState.sha = data.content.sha;\n setStatus('Committed successfully!');\n } catch (err) {\n setStatus(`Error: ${err.message}`, true);\n } finally {\n setLoading(commitBtn, false);\n }\n };\n\n // --- Clipboard Handlers ---\n const handleCopy = async () => { try { await navigator.clipboard.writeText(ghSuneState.jar.toString()); setStatus('Copied.'); } catch { setStatus('Copy failed.', true); } };\n const handleCut = async () => { try { await navigator.clipboard.writeText(ghSuneState.jar.toString()); ghSuneState.jar.updateCode(''); setStatus('Cut.'); } catch { setStatus('Cut failed.', true); } };\n const handlePaste = async () => { try { const text = await navigator.clipboard.readText(); ghSuneState.jar.updateCode(text); setStatus(`Pasted (${formatCharCount(text.length)}).`); } catch { setStatus('Paste failed.', true); } };\n\n // --- Event Listeners ---\n fetchBtn.addEventListener('click', handleFetch);\n commitBtn.addEventListener('click', handleCommit);\n copyBtn.addEventListener('click', handleCopy);\n cutBtn.addEventListener('click', handleCut);\n pasteBtn.addEventListener('click', handlePaste);\n pathInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleFetch(); } });\n\n // Initial render\n if (window.lucide) window.lucide.createIcons();\n})();\n</script>\n",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />"
}
}
]
},
{
"name": "fetch.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/fetch.sune",
"sune": [
{
"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, '&quot;')}\"\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 />"
}
}
]
},
{
"name": "git-reset.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/git-reset.sune",
"sune": [
{
"id": "pyaok99",
"name": "GitHub Reset Hard",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@main/git-reset.sune",
"updatedAt": 1756931146474,
"settings": {
"model": "openai/gpt-5",
"temperature": 1,
"top_p": 0.97,
"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 Time Machine -->\n<div id=\"gitReverterSune\" class=\"w-full max-w-3xl mx-auto p-4 md:p-6 bg-white rounded-2xl border border-gray-200 shadow-sm font-sans text-gray-800\">\n\n <!-- Styles for this Sune -->\n <style>\n /* Custom scrollbar for commit list */\n #gr_commitsList::-webkit-scrollbar { width: 6px; }\n #gr_commitsList::-webkit-scrollbar-track { background: #f1f5f9; }\n #gr_commitsList::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }\n #gr_commitsList::-webkit-scrollbar-thumb:hover { background: #94a3b8; }\n \n /* Animation for the modal */\n @keyframes fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n @keyframes scaleIn {\n from { opacity: 0; transform: scale(0.95) translateY(10px); }\n to { opacity: 1; transform: scale(1) translateY(0); }\n }\n #gr_confirmResetModal .modal-bg { animation: fadeIn 0.2s ease-out forwards; }\n #gr_confirmResetModal .modal-content { animation: scaleIn 0.2s ease-out forwards; }\n </style>\n\n <!-- Sune Header -->\n <div class=\"flex items-center justify-between pb-4 border-b border-gray-200\">\n <h1 class=\"text-xl font-bold flex items-center gap-2\">\n <i data-lucide=\"history\" class=\"w-6 h-6 text-gray-600\"></i>\n GitHub Time Machine\n </h1>\n </div>\n\n <!-- Input Form -->\n <div class=\"mt-4\">\n <label for=\"gr_repoPathInput\" class=\"text-sm font-medium text-gray-700\">Repository Path</label>\n <div class=\"mt-1 flex flex-col sm:flex-row items-stretch gap-2\">\n <input type=\"text\" id=\"gr_repoPathInput\" placeholder=\"owner/repo@branch\" class=\"flex-grow w-full px-3 py-2 bg-gray-50 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-black focus:border-black transition-all\">\n <button id=\"gr_loadCommitsBtn\" class=\"w-full sm:w-auto px-5 py-2 bg-black text-white font-semibold rounded-lg hover:bg-gray-800 active:scale-95 transition-all flex items-center justify-center gap-2 disabled:bg-gray-400 disabled:cursor-not-allowed\">\n <i data-lucide=\"arrow-down-circle\" class=\"w-4 h-4\"></i>\n <span>Load Commits</span>\n </button>\n </div>\n </div>\n\n <!-- Error and Loading Display -->\n <div id=\"gr_statusDisplay\" class=\"mt-4\"></div>\n\n <!-- Commits List -->\n <div id=\"gr_commitsContainer\" class=\"mt-4 hidden\">\n <h2 class=\"text-md font-semibold text-gray-600 mb-2\">Commit History</h2>\n <div id=\"gr_commitsList\" class=\"max-h-[50vh] overflow-y-auto border border-gray-200 rounded-lg bg-gray-50/50 divide-y divide-gray-200\">\n <!-- Commit items will be injected here -->\n </div>\n </div>\n\n <!-- Confirmation Modal -->\n <div id=\"gr_confirmResetModal\" class=\"hidden fixed inset-0 z-[100] p-4 flex items-center justify-center\">\n <div class=\"modal-bg absolute inset-0 bg-black/50 backdrop-blur-sm\"></div>\n <div class=\"modal-content relative w-full max-w-md bg-white rounded-2xl shadow-2xl p-6 border border-gray-200\">\n <div class=\"text-center\">\n <div class=\"mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100\">\n <i data-lucide=\"alert-triangle\" class=\"h-6 w-6 text-red-600\"></i>\n </div>\n <h3 class=\"mt-4 text-lg font-bold text-gray-900\">Force Push Required</h3>\n <div class=\"mt-2 text-sm text-gray-600 space-y-2\">\n <p>This is a highly destructive action. You are about to <strong class=\"font-bold text-red-700\">permanently reset</strong> the branch:</p>\n <p id=\"gr_confirmBranchInfo\" class=\"font-mono bg-gray-100 p-2 rounded-md text-xs\"></p>\n <p>This will rewrite the public history of the branch. Please type the repository name <strong id=\"gr_confirmRepoName\" class=\"font-bold\"></strong> to confirm.</p>\n </div>\n </div>\n <div class=\"mt-4\">\n <input type=\"text\" id=\"gr_confirmRepoInput\" autocomplete=\"off\" class=\"w-full text-center px-3 py-2 bg-white border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500 transition-all\">\n </div>\n <div class=\"mt-5 grid grid-cols-1 sm:grid-cols-2 gap-3\">\n <button id=\"gr_cancelResetBtn\" type=\"button\" class=\"w-full inline-flex justify-center rounded-md border border-gray-300 px-4 py-2 bg-white text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500\">\n Cancel\n </button>\n <button id=\"gr_confirmResetBtn\" type=\"button\" class=\"w-full inline-flex justify-center rounded-md border border-transparent px-4 py-2 bg-red-600 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:bg-red-300 disabled:cursor-not-allowed\">\n Reset and Force Push\n </button>\n </div>\n </div>\n </div>\n\n\n <!-- Sune Script -->\n <script>\n (() => {\n const SUNE_ID = 'sune_git_reverter';\n const GITHUB_API_BASE = 'https://api.github.com';\n\n // --- Web Worker Code ---\n const workerCode = `\n self.onmessage = async (e) => {\n const { action, payload } = e.data;\n const { path, token, sha } = payload;\n const [owner, repoWithBranch] = (path || '').split('/');\n const [repo, branch] = (repoWithBranch || '').split('@');\n\n if (!token) {\n self.postMessage({ type: 'error', message: 'GitHub token not found. Please set it in Account Settings.' });\n return;\n }\n\n if (!owner || !repo || !branch) {\n self.postMessage({ type: 'error', message: 'Invalid path format. Use: owner/repo@branch' });\n return;\n }\n\n const headers = {\n 'Authorization': \\`Bearer \\${token}\\`,\n 'Accept': 'application/vnd.github.v3+json',\n 'X-GitHub-Api-Version': '2022-11-28'\n };\n\n try {\n if (action === 'getCommits') {\n const url = \\`https://api.github.com/repos/\\${owner}/\\${repo}/commits?sha=\\${branch}\\`;\n const response = await fetch(url, { headers });\n const data = await response.json();\n\n if (!response.ok) {\n throw new Error(data.message || \\`HTTP error! status: \\${response.status}\\`);\n }\n self.postMessage({ type: 'commitsResult', data });\n\n } else if (action === 'resetBranch') {\n if (!sha) {\n throw new Error('Commit SHA is missing for reset operation.');\n }\n const url = \\`https://api.github.com/repos/\\${owner}/\\${repo}/git/refs/heads/\\${branch}\\`;\n const body = JSON.stringify({ sha, force: true });\n \n const response = await fetch(url, {\n method: 'PATCH',\n headers: { ...headers, 'Content-Type': 'application/json' },\n body: body\n });\n \n const data = await response.json();\n if (!response.ok) {\n throw new Error(data.message || \\`Failed to reset branch. Status: \\${response.status}\\`);\n }\n self.postMessage({ type: 'resetSuccess', message: 'Branch reset and force pushed successfully.' });\n }\n } catch (error) {\n self.postMessage({ type: 'error', message: error.message });\n }\n };\n `;\n\n // --- Main Sune Logic ---\n const suneRoot = document.getElementById('gitReverterSune');\n const repoPathInput = suneRoot.querySelector('#gr_repoPathInput');\n const loadCommitsBtn = suneRoot.querySelector('#gr_loadCommitsBtn');\n const statusDisplay = suneRoot.querySelector('#gr_statusDisplay');\n const commitsContainer = suneRoot.querySelector('#gr_commitsContainer');\n const commitsList = suneRoot.querySelector('#gr_commitsList');\n \n const confirmModal = suneRoot.querySelector('#gr_confirmResetModal');\n const confirmBranchInfo = suneRoot.querySelector('#gr_confirmBranchInfo');\n const confirmRepoName = suneRoot.querySelector('#gr_confirmRepoName');\n const confirmRepoInput = suneRoot.querySelector('#gr_confirmRepoInput');\n const cancelResetBtn = suneRoot.querySelector('#gr_cancelResetBtn');\n const confirmResetBtn = suneRoot.querySelector('#gr_confirmResetBtn');\n \n let worker;\n let state = {\n isLoading: false,\n commits: [],\n resetTarget: null // { sha, owner, repo, branch }\n };\n\n const getGhToken = () => localStorage.getItem('gh_token') || '';\n\n const renderStatus = (type, message) => {\n const colors = {\n error: 'bg-red-100 border-red-300 text-red-800',\n success: 'bg-green-100 border-green-300 text-green-800',\n loading: 'bg-blue-100 border-blue-300 text-blue-800'\n };\n const icons = {\n error: 'alert-circle',\n success: 'check-circle-2',\n loading: 'loader-2'\n };\n const iconClass = type === 'loading' ? 'animate-spin' : '';\n\n statusDisplay.innerHTML = `\n <div class=\"flex items-center gap-3 p-3 text-sm font-medium rounded-lg border ${colors[type]}\">\n <i data-lucide=\"${icons[type]}\" class=\"w-5 h-5 flex-shrink-0 ${iconClass}\"></i>\n <span>${message}</span>\n </div>\n `;\n window.lucide && window.lucide.createIcons();\n };\n \n const renderCommits = (commitData) => {\n if (!commitData || commitData.length === 0) {\n commitsList.innerHTML = `<div class=\"p-4 text-center text-gray-500\">No commits found for this branch.</div>`;\n return;\n }\n state.commits = commitData;\n commitsList.innerHTML = commitData.map((c, index) => {\n const msg = c.commit.message.split('\\\\n')[0];\n const author = c.commit.author;\n const shortSha = c.sha.substring(0, 7);\n const date = new Date(author.date).toLocaleString(undefined, {\n year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'\n });\n\n return `\n <div class=\"p-3 hover:bg-white/70 transition-colors duration-150\">\n <div class=\"flex items-center justify-between\">\n <div class=\"flex items-center gap-3 min-w-0\">\n ${c.author ? `<img src=\"${c.author.avatar_url}\" class=\"w-8 h-8 rounded-full bg-gray-200\" alt=\"${author.name}\">` : ''}\n <div class=\"min-w-0\">\n <p class=\"font-semibold text-gray-900 truncate\" title=\"${msg}\">${msg}</p>\n <p class=\"text-xs text-gray-500\">\n <strong>${author.name}</strong> committed on ${date}\n </p>\n </div>\n </div>\n <button data-sha=\"${c.sha}\" title=\"Reset branch to this commit\" class=\"reset-btn ml-2 flex-shrink-0 px-3 py-1.5 text-xs font-semibold text-red-700 bg-red-100 rounded-md hover:bg-red-200 active:scale-95 transition-all\">\n Reset\n </button>\n </div>\n <div class=\"mt-2 text-xs text-gray-600 font-mono bg-gray-200/50 inline-block px-2 py-0.5 rounded\">\n ${shortSha}\n </div>\n </div>\n `;\n }).join('');\n };\n\n const handleLoadCommits = () => {\n if (state.isLoading) return;\n const path = repoPathInput.value.trim();\n if (!path) {\n renderStatus('error', 'Please enter a repository path.');\n return;\n }\n\n const token = getGhToken();\n if (!token) {\n renderStatus('error', 'GitHub token not found. Please set it in Account Settings.');\n return;\n }\n\n state.isLoading = true;\n loadCommitsBtn.disabled = true;\n commitsContainer.classList.add('hidden');\n renderStatus('loading', `Fetching commits for ${path}...`);\n \n localStorage.setItem(`${SUNE_ID}_last_path`, path);\n worker.postMessage({ action: 'getCommits', payload: { path, token } });\n };\n \n const openResetModal = (sha) => {\n const [owner, repoWithBranch] = (repoPathInput.value.trim() || '').split('/');\n const [repo, branch] = (repoWithBranch || '').split('@');\n\n if (!owner || !repo || !branch) return;\n \n state.resetTarget = { sha, owner, repo, branch };\n \n confirmBranchInfo.textContent = `${owner}/${repo}@${branch}`;\n confirmRepoName.textContent = repo;\n confirmRepoInput.value = '';\n confirmResetBtn.disabled = true;\n \n confirmModal.classList.remove('hidden');\n document.body.style.overflow = 'hidden';\n confirmRepoInput.focus();\n };\n \n const closeResetModal = () => {\n confirmModal.classList.add('hidden');\n document.body.style.overflow = '';\n state.resetTarget = null;\n };\n\n const handleConfirmReset = () => {\n if (!state.resetTarget || state.isLoading) return;\n \n state.isLoading = true;\n confirmResetBtn.disabled = true;\n confirmResetBtn.innerHTML = '<i data-lucide=\"loader-2\" class=\"w-5 h-5 animate-spin mx-auto\"></i>';\n window.lucide && window.lucide.createIcons();\n\n const token = getGhToken();\n const { sha, owner, repo, branch } = state.resetTarget;\n const path = `${owner}/${repo}@${branch}`;\n\n worker.postMessage({ action: 'resetBranch', payload: { path, token, sha } });\n };\n \n function init() {\n // Setup Worker\n const workerBlob = new Blob([workerCode], { type: 'application/javascript' });\n worker = new Worker(URL.createObjectURL(workerBlob));\n \n worker.onmessage = (e) => {\n const { type, data, message } = e.data;\n state.isLoading = false;\n loadCommitsBtn.disabled = false;\n \n if (type === 'commitsResult') {\n statusDisplay.innerHTML = '';\n commitsContainer.classList.remove('hidden');\n renderCommits(data);\n } else if (type === 'resetSuccess') {\n closeResetModal();\n renderStatus('success', message);\n // Reload commits to show updated history\n setTimeout(handleLoadCommits, 1000);\n } else if (type === 'error') {\n renderStatus('error', message);\n commitsContainer.classList.add('hidden');\n }\n };\n\n // Setup Event Listeners\n loadCommitsBtn.addEventListener('click', handleLoadCommits);\n repoPathInput.addEventListener('keydown', e => e.key === 'Enter' && handleLoadCommits());\n\n commitsList.addEventListener('click', e => {\n const btn = e.target.closest('.reset-btn');\n if (btn && btn.dataset.sha) {\n openResetModal(btn.dataset.sha);\n }\n });\n \n cancelResetBtn.addEventListener('click', closeResetModal);\n confirmResetBtn.addEventListener('click', handleConfirmReset);\n confirmModal.querySelector('.modal-bg').addEventListener('click', closeResetModal);\n \n confirmRepoInput.addEventListener('input', () => {\n const matches = confirmRepoInput.value === state.resetTarget?.repo;\n confirmResetBtn.disabled = !matches;\n });\n\n // Load initial state\n repoPathInput.value = localStorage.getItem(`${SUNE_ID}_last_path`) || '';\n window.lucide && window.lucide.createIcons();\n }\n\n // Run sune\n init();\n\n })();\n </script>\n</div>\n",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />",
"script": ""
}
}
]
},
{
"name": "jsdelivr.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/jsdelivr.sune",
"sune": [
{
"id": "0in1b62",
"name": "jsdelivr",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@main/jsdelivr.sune",
"updatedAt": 1756931192100,
"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": "<div id=\"gh-to-jsdelivr-sune\" class=\"p-4 mx-auto max-w-lg w-full font-sans\">\n <div class=\"bg-white border border-gray-200 rounded-2xl shadow-sm p-4 sm:p-6 space-y-4\">\n <!-- Header -->\n <div class=\"flex items-center gap-3\">\n <div class=\"h-10 w-10 flex-shrink-0 bg-gray-900 text-white rounded-full flex items-center justify-center\">\n <i data-lucide=\"shuffle\" class=\"h-5 w-5\"></i>\n </div>\n <div>\n <h2 class=\"text-base font-semibold text-gray-900\">GitHub to jsDelivr</h2>\n <p class=\"text-sm text-gray-500\">Convert repo file paths to jsDelivr CDN links.</p>\n </div>\n </div>\n\n <!-- Input Section -->\n <div class=\"space-y-2\">\n <label for=\"sune-ghPathInput\" class=\"block text-sm font-medium text-gray-700\">GitHub Path</label>\n <input type=\"text\" id=\"sune-ghPathInput\" placeholder=\"user/repo@branch/file.js\" class=\"block w-full px-3 py-2 bg-white border border-gray-300 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent transition\">\n </div>\n\n <!-- Output Section -->\n <div class=\"space-y-2\">\n <label for=\"sune-jsdelivrUrlOutput\" class=\"block text-sm font-medium text-gray-700\">jsDelivr URL</label>\n <div class=\"relative\">\n <input type=\"text\" id=\"sune-jsdelivrUrlOutput\" readonly class=\"block w-full pr-12 pl-3 py-2 bg-gray-50 border border-gray-300 rounded-lg text-sm text-gray-600 focus:outline-none focus:ring-0 cursor-default\">\n <button id=\"sune-copyBtn\" title=\"Copy URL\" class=\"absolute inset-y-0 right-0 flex items-center justify-center h-full w-12 text-gray-500 hover:text-gray-800 transition rounded-r-lg hover:bg-gray-200 active:bg-gray-300\">\n <i data-lucide=\"copy\" class=\"h-4 w-4\"></i>\n </button>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n(() => {\n // Self-invoking function to scope variables and prevent re-initialization.\n const suneContainer = document.getElementById('gh-to-jsdelivr-sune');\n if (suneContainer.dataset.initialized) return;\n suneContainer.dataset.initialized = 'true';\n\n const ghPathInput = document.getElementById('sune-ghPathInput');\n const jsdelivrUrlOutput = document.getElementById('sune-jsdelivrUrlOutput');\n const copyBtn = document.getElementById('sune-copyBtn');\n\n // Use the unique Sune ID for localStorage key to prevent conflicts.\n const SUNE_ID = window.SUNE?.id || 'gh-to-jsdelivr-sune';\n const STORAGE_KEY = `sune_cache_${SUNE_ID}`;\n\n /**\n * Converts the GitHub path from the input field to a jsDelivr URL\n * and updates the output field. Caches the input to localStorage.\n */\n const convertAndCachePath = () => {\n const input = ghPathInput.value.trim();\n \n try {\n localStorage.setItem(STORAGE_KEY, input);\n } catch (e) {\n console.error(\"Sune Error: Failed to save to localStorage:\", e);\n }\n\n // Regex to parse: user/repo@branch/path/to/file\n const match = input.match(/^([^/]+\\/[^@]+)@([^/]+)\\/(.+)$/);\n\n if (match) {\n const [, userRepo, branch, filePath] = match;\n const url = `https://cdn.jsdelivr.net/gh/${userRepo}@${branch}/${filePath}`;\n jsdelivrUrlOutput.value = url;\n } else {\n jsdelivrUrlOutput.value = '';\n }\n };\n\n /**\n * Copies the content of the output field to the clipboard and provides user feedback.\n */\n const copyToClipboard = async () => {\n const url = jsdelivrUrlOutput.value;\n if (!url) return;\n\n try {\n await navigator.clipboard.writeText(url);\n const originalIcon = copyBtn.innerHTML;\n copyBtn.innerHTML = '<i data-lucide=\"check\" class=\"h-4 w-4 text-green-600\"></i>';\n window.lucide?.createIcons(); // Re-render the new icon\n\n setTimeout(() => {\n copyBtn.innerHTML = originalIcon;\n window.lucide?.createIcons(); // Restore the original icon\n }, 2000);\n } catch (err) {\n console.error('Sune Error: Failed to copy text: ', err);\n // Fallback for older browsers or insecure contexts\n alert('Failed to copy URL. Please copy it manually.');\n }\n };\n\n /**\n * Initializes the sune by loading cached data and setting up event listeners.\n */\n const init = () => {\n // Load cached input from localStorage on initialization.\n try {\n const cachedInput = localStorage.getItem(STORAGE_KEY);\n if (cachedInput) {\n ghPathInput.value = cachedInput;\n convertAndCachePath(); // Convert immediately if cached data exists.\n }\n } catch (e) {\n console.error(\"Sune Error: Failed to read from localStorage:\", e);\n }\n\n // Add event listeners for real-time conversion and copy functionality.\n ghPathInput.addEventListener('input', convertAndCachePath);\n copyBtn.addEventListener('click', copyToClipboard);\n \n // Ensure Lucide icons are rendered after the sune is injected.\n if (window.lucide) {\n window.lucide.createIcons();\n }\n };\n\n // Defer initialization slightly to ensure the DOM is fully ready.\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init);\n } else {\n init();\n }\n\n})();\n</script>\n",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />"
}
}
]
},
{
"name": "marketplace.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/marketplace.sune",
"sune": [
{
"id": "e1yibwd",
"name": "Marketplace",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@main/marketplace.sune",
"updatedAt": 1756931159110,
"settings": {
"model": "openai/gpt-5",
"temperature": 1,
"top_p": 0.97,
"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": "",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />"
}
}
]
},
{
"name": "storage.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/storage.sune",
"sune": [
{
"id": "gjfq54x",
"name": "StorageManager",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@main/storage.sune",
"updatedAt": 1756931131942,
"settings": {
"model": "openai/gpt-5",
"temperature": 1,
"top_p": 0.97,
"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: Storage Inspector -->\n<div id=\"storage-inspector-sune\" class=\"w-full mx-auto max-w-none p-3 sm:p-4 bg-white rounded-lg border border-gray-200 shadow-sm text-sm\">\n <div class=\"flex items-center justify-between mb-4\">\n <h2 class=\"text-lg font-semibold text-gray-800 flex items-center gap-2\">\n <i data-lucide=\"database\" class=\"h-5 w-5\"></i>\n <span>Storage Inspector</span>\n </h2>\n <button id=\"si-refresh-btn\" class=\"p-2 rounded-lg hover:bg-gray-100 active:bg-gray-200 transition-colors\">\n <i data-lucide=\"refresh-cw\" class=\"h-5 w-5 text-gray-600\"></i>\n </button>\n </div>\n\n <!-- Storage Overview -->\n <div id=\"si-overview\" class=\"mb-4 p-4 bg-gray-50 rounded-lg border border-gray-200\">\n <h3 class=\"font-medium text-gray-700 mb-2\">Device Storage Quota</h3>\n <div id=\"si-overview-content\" class=\"space-y-2\">\n <p class=\"text-xs text-gray-500\">Calculating...</p>\n </div>\n </div>\n\n <!-- Tabs -->\n <div class=\"border-b border-gray-200 mb-4\">\n <nav class=\"-mb-px flex space-x-4\" aria-label=\"Tabs\">\n <button id=\"si-tab-ls\" data-tab=\"ls\" class=\"si-tab-btn whitespace-nowrap py-3 px-1 border-b-2 font-medium border-slate-600 text-slate-700\">\n localStorage\n </button>\n <button id=\"si-tab-idb\" data-tab=\"idb\" class=\"si-tab-btn whitespace-nowrap py-3 px-1 border-b-2 font-medium border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300\">\n IndexedDB (localforage)\n </button>\n </nav>\n </div>\n\n <!-- Loading Indicator -->\n <div id=\"si-loading\" class=\"hidden text-center p-8\">\n <p class=\"text-gray-500 flex items-center justify-center gap-2\">\n <i data-lucide=\"loader\" class=\"h-5 w-5 animate-spin\"></i>\n <span>Inspecting storage...</span>\n </p>\n </div>\n \n <!-- Content Panels -->\n <div id=\"si-panels\">\n <div id=\"si-panel-ls\" class=\"si-panel space-y-2\">\n <!-- localStorage content will be injected here -->\n </div>\n <div id=\"si-panel-idb\" class=\"si-panel hidden space-y-2\">\n <!-- IndexedDB content will be injected here -->\n </div>\n </div>\n\n <script>\n (() => {\n const suneId = 'storage-inspector-sune';\n const root = document.getElementById(suneId);\n if (!root) return;\n\n const overviewEl = root.querySelector('#si-overview-content');\n const refreshBtn = root.querySelector('#si-refresh-btn');\n const loadingEl = root.querySelector('#si-loading');\n const panelsContainer = root.querySelector('#si-panels');\n const tabs = {\n ls: { btn: root.querySelector('#si-tab-ls'), panel: root.querySelector('#si-panel-ls') },\n idb: { btn: root.querySelector('#si-tab-idb'), panel: root.querySelector('#si-panel-idb') }\n };\n\n let worker;\n\n // --- Utility Functions ---\n const formatBytes = (bytes, decimals = 2) => {\n if (bytes === 0) return '0 Bytes';\n const k = 1024;\n const dm = decimals < 0 ? 0 : decimals;\n const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];\n };\n\n const sanitizeHTML = (str) => {\n const temp = document.createElement('div');\n temp.textContent = str;\n return temp.innerHTML;\n };\n\n // --- Worker Setup ---\n const createWorker = () => {\n const workerCode = `\n self.onmessage = (e) => {\n const { task, data } = e.data;\n if (task === 'calculateSizes') {\n const results = {\n localStorage: { items: [], totalSize: 0 },\n indexedDB: { items: [], totalSize: 0 }\n };\n\n const calculateSize = (value) => {\n // Using Blob size is more accurate for byte representation\n try {\n return new Blob([JSON.stringify(value)]).size;\n } catch (err) {\n return new Blob([String(value)]).size;\n }\n };\n\n // Process localStorage\n if (data.localStorage) {\n for (const item of data.localStorage) {\n const size = calculateSize(item.value);\n results.localStorage.items.push({ ...item, size });\n results.localStorage.totalSize += size;\n }\n results.localStorage.items.sort((a, b) => b.size - a.size);\n }\n\n // Process IndexedDB\n if (data.indexedDB) {\n for (const item of data.indexedDB) {\n const size = calculateSize(item.value);\n results.indexedDB.items.push({ ...item, size });\n results.indexedDB.totalSize += size;\n }\n results.indexedDB.items.sort((a, b) => b.size - a.size);\n }\n \n self.postMessage(results);\n }\n };\n `;\n const blob = new Blob([workerCode], { type: 'application/javascript' });\n return new Worker(URL.createObjectURL(blob));\n };\n \n // --- UI Rendering ---\n const renderItem = (item, type, dbName = '') => {\n const valueStr = JSON.stringify(item.value, null, 2);\n const valuePreview = valueStr.length > 100 ? valueStr.substring(0, 100) + '...' : valueStr;\n\n return `\n <div class=\"border border-gray-200 rounded-lg overflow-hidden\">\n <details class=\"group\">\n <summary class=\"flex items-center justify-between p-3 cursor-pointer hover:bg-gray-50 list-none\">\n <div class=\"flex items-center gap-3 overflow-hidden\">\n <span class=\"font-mono text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded\">${sanitizeHTML(item.key)}</span>\n ${dbName ? `<span class=\"text-xs text-gray-400 font-medium hidden sm:inline-block\">(${dbName})</span>` : ''}\n </div>\n <div class=\"flex items-center gap-3 flex-shrink-0\">\n <span class=\"text-gray-500 font-medium\">${formatBytes(item.size)}</span>\n <button data-type=\"${type}\" data-key=\"${sanitizeHTML(item.key)}\" data-db=\"${dbName}\" class=\"si-delete-btn p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md\">\n <i data-lucide=\"trash-2\" class=\"h-4 w-4\"></i>\n </button>\n <i data-lucide=\"chevron-down\" class=\"h-5 w-5 text-gray-400 group-open:rotate-180 transition-transform\"></i>\n </div>\n </summary>\n <div class=\"p-3 border-t bg-white\">\n <pre class=\"text-xs p-3 bg-gray-800 text-white rounded-md overflow-auto max-h-60\"><code>${sanitizeHTML(valueStr)}</code></pre>\n </div>\n </details>\n </div>\n `;\n };\n\n const renderPanel = (panel, items, type, totalSize) => {\n let html = `<div class=\"mb-3 p-2 bg-slate-50 rounded-md text-slate-600 font-medium text-xs\">Total Size: ${formatBytes(totalSize)}</div>`;\n if (items.length === 0) {\n html += '<p class=\"text-center text-gray-500 py-4\">No items found.</p>';\n } else {\n html += items.map(item => renderItem(item, type, item.dbName)).join('');\n }\n panel.innerHTML = html;\n };\n\n const updateOverview = async () => {\n if ('storage' in navigator && 'estimate' in navigator.storage) {\n try {\n const estimate = await navigator.storage.estimate();\n const usage = estimate.usage || 0;\n const quota = estimate.quota || 0;\n const percent = quota > 0 ? (usage / quota * 100).toFixed(2) : 0;\n \n overviewEl.innerHTML = `\n <div class=\"w-full bg-gray-200 rounded-full h-2.5 mb-2\">\n <div class=\"bg-blue-600 h-2.5 rounded-full\" style=\"width: ${percent}%\"></div>\n </div>\n <div class=\"flex justify-between text-xs text-gray-600\">\n <span>Used: <strong>${formatBytes(usage)}</strong></span>\n <span>Quota: <strong>${formatBytes(quota)}</strong></span>\n <span><strong>${percent}%</strong></span>\n </div>\n `;\n } catch (error) {\n overviewEl.innerHTML = '<p class=\"text-xs text-red-500\">Could not estimate storage. API may be unsupported or blocked.</p>';\n console.error('Storage estimation failed:', error);\n }\n } else {\n overviewEl.innerHTML = '<p class=\"text-xs text-gray-500\">Storage Estimation API not supported.</p>';\n }\n };\n \n // --- Data Fetching and Processing ---\n const refreshData = async () => {\n loadingEl.classList.remove('hidden');\n panelsContainer.classList.add('hidden');\n \n await updateOverview();\n\n // 1. Read data from main thread\n const dataToProcess = {\n localStorage: [],\n indexedDB: []\n };\n\n // Read localStorage\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n try {\n dataToProcess.localStorage.push({ key, value: JSON.parse(localStorage.getItem(key)) });\n } catch (e) {\n dataToProcess.localStorage.push({ key, value: localStorage.getItem(key) });\n }\n }\n\n // Read IndexedDB (via localforage)\n const dbNames = ['localforage', 'master_cache']; // From main app's source\n const idbPromises = dbNames.map(name => {\n return new Promise(resolve => {\n const store = localforage.createInstance({ name });\n const items = [];\n store.iterate((value, key) => {\n items.push({ key, value, dbName: name });\n }).then(() => resolve(items)).catch(() => resolve([])); // Resolve even on error\n });\n });\n\n const idbResults = await Promise.all(idbPromises);\n dataToProcess.indexedDB = idbResults.flat();\n\n // 2. Post to worker for calculation\n worker.postMessage({ task: 'calculateSizes', data: dataToProcess });\n };\n\n // --- Event Handlers ---\n const handleWorkerMessage = (e) => {\n const results = e.data;\n renderPanel(tabs.ls.panel, results.localStorage.items, 'ls', results.localStorage.totalSize);\n renderPanel(tabs.idb.panel, results.indexedDB.items, 'idb', results.indexedDB.totalSize);\n \n loadingEl.classList.add('hidden');\n panelsContainer.classList.remove('hidden');\n\n if (window.lucide) window.lucide.createIcons();\n };\n\n const handleTabClick = (e) => {\n const targetTab = e.target.closest('.si-tab-btn').dataset.tab;\n Object.keys(tabs).forEach(key => {\n const isActive = key === targetTab;\n tabs[key].btn.classList.toggle('border-slate-600', isActive);\n tabs[key].btn.classList.toggle('text-slate-700', isActive);\n tabs[key].btn.classList.toggle('border-transparent', !isActive);\n tabs[key].btn.classList.toggle('text-gray-500', !isActive);\n tabs[key].panel.classList.toggle('hidden', !isActive);\n });\n };\n\n const handleDeleteClick = async (e) => {\n const button = e.target.closest('.si-delete-btn');\n if (!button) return;\n\n const { type, key, db } = button.dataset;\n if (confirm(`Are you sure you want to delete \"${key}\"?`)) {\n if (type === 'ls') {\n localStorage.removeItem(key);\n } else if (type === 'idb' && db) {\n const store = localforage.createInstance({ name: db });\n await store.removeItem(key);\n }\n await refreshData();\n }\n };\n \n // --- Initialization ---\n const init = () => {\n worker = createWorker();\n worker.onmessage = handleWorkerMessage;\n \n refreshBtn.addEventListener('click', refreshData);\n tabs.ls.btn.addEventListener('click', handleTabClick);\n tabs.idb.btn.addEventListener('click', handleTabClick);\n panelsContainer.addEventListener('click', handleDeleteClick);\n\n // Initial load\n refreshData();\n if (window.lucide) window.lucide.createIcons();\n };\n \n init();\n\n // Cleanup when sune is removed from DOM (if needed)\n const observer = new MutationObserver((mutationsList) => {\n for (const mutation of mutationsList) {\n if (mutation.removedNodes) {\n mutation.removedNodes.forEach((removedNode) => {\n if (removedNode === root || removedNode.contains(root)) {\n if (worker) worker.terminate();\n observer.disconnect();\n }\n });\n }\n }\n });\n observer.observe(document.body, { childList: true, subtree: true });\n\n })();\n </script>\n</div>\n",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />",
"script": ""
}
}
]
},
{
"name": "sw.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/sw.sune",
"sune": [
{
"id": "tbkc6m5",
"name": "SW",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@main/sw.sune",
"updatedAt": 1756931099525,
"settings": {
"model": "openai/gpt-5-chat",
"temperature": 1,
"top_p": 0.97,
"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: Service Worker detector + UI -->\n<div id=\"sune-sw-detector\" data-sw=\"/sw.js\" class=\"max-w-md mx-auto p-3\">\n <div class=\"flex items-center gap-3\">\n <div id=\"swdot\" class=\"h-3 w-3 rounded-full bg-gray-300\"></div>\n <div class=\"flex-1\">\n <div class=\"text-sm font-medium\" id=\"sw-status\">Checking service worker…</div>\n <div class=\"text-xs text-gray-500\" id=\"sw-sub\">—</div>\n </div>\n <button id=\"sw-toggle\" class=\"ml-2 px-2 py-1 rounded-lg bg-gray-100 text-sm\">…</button>\n </div>\n\n <div class=\"mt-3 grid grid-cols-3 gap-2\">\n <button id=\"sw-register\" class=\"col-span-1 rounded-lg bg-black text-white py-2 text-sm\">Register</button>\n <button id=\"sw-unregister\" class=\"col-span-1 rounded-lg bg-red-50 text-red-700 py-2 text-sm hidden\">Unregister</button>\n <button id=\"sw-ping\" class=\"col-span-1 rounded-lg bg-gray-100 py-2 text-sm\">Ping SW</button>\n </div>\n\n <div class=\"mt-3 text-xs text-gray-600\">\n <div class=\"flex items-center gap-2\">\n <i data-lucide=\"info\" class=\"h-4 w-4\"></i>\n <span>SW file: <code id=\"sw-path\" class=\"text-xs font-mono\"></code></span>\n </div>\n <div class=\"mt-2\">\n <div class=\"text-xs font-medium\">Message log</div>\n <div id=\"sw-log\" class=\"mt-1 text-xs bg-gray-50 border border-gray-200 rounded-lg p-2 max-h-32 overflow-auto\"></div>\n </div>\n </div>\n</div>\n\n<script>\n(() => {\n const root = document.getElementById('sune-sw-detector');\n if (!root) return;\n const swPath = root.dataset.sw || '/sw.js';\n const el = {\n dot: root.querySelector('#swdot'),\n status: root.querySelector('#sw-status'),\n sub: root.querySelector('#sw-sub'),\n toggle: root.querySelector('#sw-toggle'),\n regBtn: root.querySelector('#sw-register'),\n unregBtn: root.querySelector('#sw-unregister'),\n pingBtn: root.querySelector('#sw-ping'),\n path: root.querySelector('#sw-path'),\n log: root.querySelector('#sw-log')\n };\n\n el.path.textContent = swPath;\n\n const log = (t, cls = '') => {\n const d = document.createElement('div');\n d.className = 'leading-5 ' + cls;\n d.textContent = (new Date()).toLocaleTimeString() + ' — ' + t;\n el.log.prepend(d);\n };\n\n const setStatus = ({supported,falseMsg,registered,active,scope,controller}) => {\n if (!supported) {\n el.dot.className = 'h-3 w-3 rounded-full bg-red-200';\n el.status.textContent = 'Service workers not supported';\n el.sub.textContent = falseMsg || 'Requires HTTPS or localhost.';\n el.regBtn.classList.add('hidden');\n el.unregBtn.classList.add('hidden');\n el.pingBtn.classList.add('hidden');\n el.toggle.textContent = 'n/a';\n return;\n }\n if (registered) {\n el.dot.className = active ? 'h-3 w-3 rounded-full bg-emerald-400' : 'h-3 w-3 rounded-full bg-yellow-300';\n el.status.textContent = active ? 'Service worker active' : 'Service worker registered (not controlling page)';\n el.sub.textContent = scope ? `scope: ${scope}` : 'registered';\n el.regBtn.classList.add('hidden');\n el.unregBtn.classList.remove('hidden');\n el.pingBtn.classList.remove('hidden');\n el.toggle.textContent = 'Registered';\n } else {\n el.dot.className = 'h-3 w-3 rounded-full bg-gray-300';\n el.status.textContent = 'No service worker registered';\n el.sub.textContent = 'Click Register to add one at ' + swPath;\n el.regBtn.classList.remove('hidden');\n el.unregBtn.classList.add('hidden');\n el.pingBtn.classList.remove('hidden');\n el.toggle.textContent = 'Not registered';\n }\n };\n\n // Detect support + current registration\n async function detect() {\n const supported = 'serviceWorker' in navigator;\n if (!supported) {\n setStatus({supported:false, falseMsg:'Service workers are not supported by this browser.'});\n log('ServiceWorker API not available');\n return;\n }\n try {\n // find a registration that matches the URL (fallback to getRegistrations)\n let reg = await navigator.serviceWorker.getRegistration(swPath);\n if (!reg) {\n const all = await navigator.serviceWorker.getRegistrations();\n // if multiple registered, prefer one that contains swPath in its scope or scriptURL\n reg = all.find(r => r?.scope?.includes('/') || r?.active?.scriptURL?.includes(swPath)) || null;\n }\n const active = !!(reg && (reg.active || navigator.serviceWorker.controller));\n const controlling = !!navigator.serviceWorker.controller;\n setStatus({supported:true, registered:!!reg, active:controlling || active, scope: reg?.scope || ''});\n log('Detected registration: ' + (reg ? (reg.scope || reg.active?.scriptURL || 'registered') : 'none'));\n } catch (err) {\n setStatus({supported:true, registered:false, active:false});\n log('Error detecting registration: ' + (err && err.message ? err.message : String(err)), 'text-red-600');\n }\n }\n\n // register sw\n async function registerSW() {\n if (!('serviceWorker' in navigator)) return;\n try {\n log('Registering ' + swPath + ' …');\n const reg = await navigator.serviceWorker.register(swPath, {scope: undefined});\n log('Registered: ' + (reg.scope || reg.installing?.scriptURL || 'ok'));\n // optionally wait until it's active\n await navigator.serviceWorker.ready;\n log('Service worker ready');\n await detect();\n } catch (err) {\n log('Register failed: ' + err.message, 'text-red-600');\n console.error(err);\n }\n }\n\n // unregister any registration matching swPath or all\n async function unregisterSW() {\n if (!('serviceWorker' in navigator)) return;\n try {\n log('Unregistering registrations…');\n const regs = await navigator.serviceWorker.getRegistrations();\n let count = 0;\n for (const r of regs) {\n // try to only remove the ones whose scriptURL includes swPath (fallback: remove all)\n if (!swPath || (r?.active?.scriptURL && r.active.scriptURL.includes(swPath)) || (r?.scope && r.scope.includes('/'))) {\n const ok = await r.unregister();\n if (ok) count++;\n }\n }\n log('Unregistered ' + count + ' registration(s)');\n await detect();\n } catch (err) {\n log('Unregister failed: ' + err.message, 'text-red-600');\n console.error(err);\n }\n }\n\n // Ping SW via MessageChannel and wait for reply\n function pingSW() {\n if (!('serviceWorker' in navigator)) return log('No SW support', 'text-red-600');\n const controller = navigator.serviceWorker.controller;\n if (!controller) {\n log('No controller (page not controlled by a SW). A reload might be required.', 'text-yellow-600');\n return;\n }\n const mc = new MessageChannel();\n const timeout = setTimeout(() => {\n log('Ping timed out (no response)', 'text-red-600');\n mc.port1.onmessage = null;\n }, 4000);\n mc.port1.onmessage = ev => {\n clearTimeout(timeout);\n log('PONG from SW: ' + JSON.stringify(ev.data));\n };\n try {\n controller.postMessage({type: 'PING', ts: Date.now()}, [mc.port2]);\n log('PING -> SW');\n } catch (err) {\n clearTimeout(timeout);\n log('Ping failed: ' + err.message, 'text-red-600');\n }\n }\n\n // listeners\n el.regBtn.addEventListener('click', async () => { await registerSW(); });\n el.unregBtn.addEventListener('click', async () => {\n if (!confirm('Unregister service workers for this origin?')) return;\n await unregisterSW();\n });\n el.pingBtn.addEventListener('click', () => pingSW());\n\n // react to controllerchange (page becomes controlled)\n navigator.serviceWorker && navigator.serviceWorker.addEventListener && navigator.serviceWorker.addEventListener('controllerchange', () => {\n log('controllerchange event');\n setTimeout(detect, 300);\n });\n\n // listen for messages from SW (fallback path if service worker broadcasts)\n navigator.serviceWorker && navigator.serviceWorker.addEventListener && navigator.serviceWorker.addEventListener('message', ev => {\n log('Message from SW: ' + JSON.stringify(ev.data));\n });\n\n // initial detect\n detect();\n\n // expose for debugging (optional)\n root.sune = {detect, registerSW, unregisterSW, pingSW};\n})();\n</script>",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />",
"script": ""
}
}
]
},
{
"name": "sync.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/sync.sune",
"sune": [
{
"id": "csx7463",
"name": "Github Sync",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@master/sync.sune",
"updatedAt": 1756929108419,
"settings": {
"model": "openai/gpt-5",
"temperature": 1,
"top_p": 0.97,
"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": "<div id=\"githubSyncSune\" class=\"p-4 mx-4 mb-4 bg-gray-50/50 border rounded-lg\">\n <!-- Sune version v1.4.0 -->\n <div class=\"space-y-3\">\n <div>\n <p id=\"infoText\" class=\"p-3 text-sm text-center text-gray-600 border border-dashed rounded-md break-all\"></p>\n </div>\n <div>\n <button id=\"syncBtn\" class=\"inline-flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors\">\n <!-- Content dynamically set by script -->\n </button>\n </div>\n <div>\n <label for=\"logArea\" class=\"block mb-1 text-xs font-medium text-gray-500\">Sync Logs</label>\n <pre id=\"logArea\" class=\"w-full h-32 p-2.5 overflow-auto font-mono text-xs text-gray-600 bg-white border border-gray-200 rounded-md whitespace-pre-wrap\"></pre>\n </div>\n </div>\n</div>\n\n<script>\n(() => {\n // --- Configuration ---\n const hideUI = true // Set to true to hide the sync UI panel below the sync icon.\n // -------------------\n\n const suneContainer = document.getElementById('githubSyncSune');\n if (!suneContainer || suneContainer.dataset.initialized) return;\n suneContainer.dataset.initialized = 'true';\n\n const infoText = suneContainer.querySelector('#infoText');\n const syncBtn = suneContainer.querySelector('#syncBtn');\n const logArea = suneContainer.querySelector('#logArea');\n const mainSyncButton = document.getElementById('syncSune');\n\n if (!window.SUNE || !window.USER) {\n if (logArea) logArea.textContent = 'ERROR: Core Sune environment not found.';\n return;\n }\n\n const activeSune = window.SUNE.active;\n if (!activeSune) {\n if (logArea) logArea.textContent = 'Error: No active sune found.';\n return;\n }\n\n let worker = null;\n let syncTimeout = null;\n const state = {\n isBusy: false,\n remoteSha: null,\n pathInfo: null,\n syncAction: 'none',\n forceSync: false,\n };\n\n const log = (message) => {\n if (!logArea || hideUI) return;\n const timestamp = new Date().toLocaleTimeString();\n logArea.textContent += `[${timestamp}] ${message}\\n`;\n logArea.scrollTop = logArea.scrollHeight;\n };\n \n const updateMainSyncBadge = (text) => {\n const badge = document.getElementById('suneSyncBadge');\n if (!badge) return;\n\n if (text) {\n badge.textContent = text;\n badge.classList.remove('hidden');\n } else {\n badge.classList.add('hidden');\n }\n };\n\n const clearSyncTimeout = () => {\n if (syncTimeout) {\n clearTimeout(syncTimeout);\n syncTimeout = null;\n }\n };\n\n const setBusy = (busy) => {\n state.isBusy = busy;\n const icon = mainSyncButton?.querySelector('svg');\n if (icon) {\n if (busy) {\n icon.classList.add('animate-spin');\n } else {\n icon.classList.remove('animate-spin');\n }\n }\n if (!busy) {\n clearSyncTimeout();\n updateMainSyncBadge(null); // Clear badge on any finish.\n }\n };\n\n const updateSyncButtonUI = (action, sha) => {\n state.syncAction = action;\n if (action !== 'synced') {\n state.remoteSha = sha;\n }\n if (hideUI) return; // Don't update UI if hidden\n \n switch (action) {\n case 'upload':\n syncBtn.disabled = state.isBusy;\n syncBtn.innerHTML = `<i data-lucide=\"upload-cloud\" class=\"w-5 h-5 mr-2\"></i><span>${sha ? 'Upload Changes' : 'Create on GitHub'}</span>`;\n break;\n case 'synced':\n syncBtn.disabled = true;\n syncBtn.innerHTML = `<i data-lucide=\"check-circle-2\" class=\"w-5 h-5 mr-2\"></i><span>In Sync</span>`;\n break;\n default: // 'none' or error\n syncBtn.disabled = true;\n syncBtn.innerHTML = `<i data-lucide=\"help-circle\" class=\"w-5 h-5 mr-2\"></i><span>Configure Sync Path</span>`;\n break;\n }\n if (window.lucide) window.lucide.createIcons();\n };\n\n const cleanup = () => {\n clearSyncTimeout();\n if (worker) {\n worker.terminate();\n worker = null;\n }\n if (mainSyncButton && mainSyncButton.__syncClickListener) {\n mainSyncButton.removeEventListener('click', mainSyncButton.__syncClickListener);\n delete mainSyncButton.__syncClickListener;\n }\n suneContainer.dataset.initialized = ''; // Allow re-init on next open\n };\n\n const createWorker = () => {\n const workerCode = `\n const GITHUB_API = 'https://api.github.com';\n\n async function apiCall(method, path, token, body = null) {\n const headers = { 'Authorization': \\`token \\${token}\\`, 'Accept': 'application/vnd.github.v3+json' };\n if (body) headers['Content-Type'] = 'application/json';\n const options = { method, headers, body: body ? JSON.stringify(body) : null };\n try {\n const res = await fetch(GITHUB_API + path, options);\n const data = res.status === 204 ? {} : await res.json().catch(() => ({}));\n return { ok: res.ok, status: res.status, data };\n } catch (error) {\n return { ok: false, status: 0, error: error.message };\n }\n }\n\n self.onmessage = async (e) => {\n const { type, pat, pathInfo, suneContentB64, sha, suneName } = e.data;\n if (!pathInfo) return self.postMessage({ type: 'error', reason: 'pathInfo is missing.' });\n const { owner, repo, branch, path } = pathInfo;\n\n if (!pat) {\n return self.postMessage({ type: 'error', reason: 'GitHub PAT not found.' });\n }\n \n const refParam = branch ? \\`?ref=\\${encodeURIComponent(branch)}\\` : '';\n const contentsPath = \\`/repos/\\${owner}/\\${repo}/contents/\\${path}\\${refParam}\\`;\n \n if (type === 'check') {\n const contentRes = await apiCall('GET', contentsPath, pat);\n\n if (contentRes.status === 404) {\n return self.postMessage({ type: 'status_report', exists: false });\n }\n if (!contentRes.ok) {\n self.postMessage({ type: 'log', message: \\`Error (\\${contentRes.status}) fetching file: \\${contentRes.data?.message || contentRes.error}\\` });\n return self.postMessage({ type: 'error', reason: 'Content fetch failed' });\n }\n \n const commitSha = branch || 'HEAD';\n const commitsPath = \\`/repos/\\${owner}/\\${repo}/commits?sha=\\${commitSha}&path=\\${encodeURIComponent(path)}&page=1&per_page=1\\`;\n const commitRes = await apiCall('GET', commitsPath, pat);\n const commitDate = (commitRes.ok && commitRes.data.length > 0) ? commitRes.data[0].commit.committer.date : null;\n\n return self.postMessage({ type: 'status_report', exists: true, sha: contentRes.data.sha, commitDate });\n }\n \n if (type === 'fetch_content') {\n const res = await apiCall('GET', contentsPath, pat);\n if (res.ok) return self.postMessage({ type: 'content_fetched', contentB64: res.data.content, newSha: res.data.sha });\n return self.postMessage({ type: 'error', reason: 'Fetch failed' });\n }\n\n if (type === 'sync') {\n const commitMessage = sha ? \\`Sync: Update sune '\\${suneName}'\\` : \\`Sync: Create sune '\\${suneName}'\\`;\n const body = { message: commitMessage, content: suneContentB64 };\n if (sha) body.sha = sha;\n if (branch) body.branch = branch;\n \n self.postMessage({ type: 'log', message: \\`Committing to \\${owner}/\\${repo}...\\`});\n const res = await apiCall('PUT', \\`/repos/\\${owner}/\\${repo}/contents/\\${path}\\`, pat, body);\n\n if (res.ok) {\n const newSha = res.data.content.sha;\n self.postMessage({ type: 'log', message: \\`✅ Sync successful! New SHA: \\${newSha.substring(0,7)}\\` });\n return self.postMessage({ type: 'sync_complete', newSha });\n }\n self.postMessage({ type: 'log', message: \\`ERROR (\\${res.status}): \\${res.data?.message || res.error || 'Sync failed.'}\\` });\n return self.postMessage({ type: 'error', reason: res.data?.message || 'Sync failed' });\n }\n };\n `;\n const blob = new Blob([workerCode], { type: 'application/javascript' });\n return new Worker(URL.createObjectURL(blob));\n };\n \n const startWorkerTask = (message, timeout = 20000) => {\n if (!worker) {\n log('ERROR: Worker is not initialized.');\n setBusy(false);\n return;\n }\n clearSyncTimeout();\n worker.postMessage(message);\n syncTimeout = setTimeout(() => {\n log('ERROR: Sync operation timed out. Terminating task.');\n setBusy(false);\n cleanup();\n init(); // Re-initialize for the next attempt\n log('Sync has been reset. Please try again.');\n }, timeout);\n };\n\n const performUpload = () => {\n if (!state.pathInfo) return;\n const pat = window.USER?.PAT;\n if (!pat) {\n log('ERROR: GitHub PAT not found in Account Settings.');\n return;\n }\n\n setBusy(true);\n updateMainSyncBadge('Up');\n if(!hideUI) {\n syncBtn.disabled = true;\n syncBtn.innerHTML = `<i data-lucide=\"loader-2\" class=\"w-5 h-5 mr-2 animate-spin\"></i><span>Uploading...</span>`;\n if (window.lucide) window.lucide.createIcons();\n }\n log('Preparing sune data for upload...');\n \n try {\n const suneToSync = JSON.parse(JSON.stringify(window.SUNE.active));\n const suneJson = JSON.stringify([suneToSync], null, 2);\n const suneContentB64 = btoa(unescape(encodeURIComponent(suneJson)));\n startWorkerTask({ type: 'sync', pat, pathInfo: state.pathInfo, suneContentB64, suneName: window.SUNE.active.name, sha: state.remoteSha });\n } catch (error) {\n log(`FATAL: Failed to prepare sune data. ${error.message}`);\n setBusy(false);\n }\n };\n \n const handleWorkerMessage = (e) => {\n clearSyncTimeout();\n const { type } = e.data;\n if (type === 'log') return log(e.data.message);\n \n if (type === 'error') {\n log(`Worker error: ${e.data.reason || 'Unknown'}`);\n setBusy(false);\n updateSyncButtonUI('upload', state.remoteSha);\n return;\n }\n\n if (type === 'status_report') {\n const { exists, sha, commitDate } = e.data;\n const forceSync = state.forceSync;\n state.forceSync = false; // Reset force flag\n\n if (!exists) {\n log('File not found on GitHub. Ready to create.');\n setBusy(false);\n updateSyncButtonUI('upload', null);\n if (forceSync) {\n log('Force sync: creating file on GitHub...');\n performUpload();\n }\n } else {\n const localUpdate = window.SUNE.active.updatedAt;\n const remoteUpdate = commitDate ? new Date(commitDate).getTime() : 0;\n \n if (forceSync) {\n log('Force sync: uploading local version...');\n setBusy(false);\n updateSyncButtonUI('upload', sha);\n performUpload();\n } else if (remoteUpdate > localUpdate + 5000) { // 5s buffer\n log('Remote is newer. Auto-downloading...');\n setBusy(true);\n updateMainSyncBadge('Down');\n startWorkerTask({ type: 'fetch_content', pat: window.USER.PAT, pathInfo: state.pathInfo });\n } else if (localUpdate > remoteUpdate + 5000) {\n log('Local is newer. Ready to upload.');\n setBusy(false);\n updateSyncButtonUI('upload', sha);\n } else {\n log('✅ Sune is in sync.');\n setBusy(false);\n updateSyncButtonUI('synced', sha);\n }\n }\n } else if (type === 'content_fetched') {\n try {\n const decodedJson = decodeURIComponent(escape(atob(e.data.contentB64)));\n const remoteSuneArr = JSON.parse(decodedJson);\n if (!Array.isArray(remoteSuneArr) || !remoteSuneArr[0]?.id) throw new Error(\"Invalid sune format in remote file.\");\n \n const remoteSune = remoteSuneArr[0];\n const currentSune = window.SUNE.active;\n \n Object.assign(currentSune, { ...remoteSune, settings: { ...currentSune.settings, ...remoteSune.settings } });\n currentSune.updatedAt = Date.now();\n window.SUNE.save();\n\n log('✅ Sune updated from GitHub.');\n updateSyncButtonUI('synced', e.data.newSha);\n\n if (window.closeSettings) window.closeSettings();\n if (window.renderSidebar) window.renderSidebar();\n if (window.reflectActiveSune) window.reflectActiveSune();\n\n } catch (err) {\n log(`ERROR processing download: ${err.message}`);\n } finally {\n setBusy(false);\n }\n } else if (type === 'sync_complete') {\n window.SUNE.active.updatedAt = Date.now();\n window.SUNE.save();\n setBusy(false);\n updateSyncButtonUI('synced', e.data.newSha);\n }\n };\n\n const startCheck = (isManual = false) => {\n const pathInfo = parseGhPath(window.SUNE.active.url);\n if (!pathInfo) return;\n state.pathInfo = pathInfo;\n\n if (!window.USER?.PAT) {\n log('ERROR: GitHub PAT not found in Account Settings.');\n return;\n }\n\n setBusy(true);\n if (isManual) log('Checking remote status...');\n startWorkerTask({ type: 'check', pat: window.USER.PAT, pathInfo: state.pathInfo });\n };\n\n const parseGhPath = (path) => {\n const match = (path || '').trim().match(/^gh:\\/\\/([^\\/]+)\\/([^@\\/]+)(?:@([^\\/]+))?\\/(.+)$/);\n return match ? { owner: match[1], repo: match[2], branch: match[3], path: match[4] } : null;\n };\n\n const handleMainSyncClick = (e) => {\n e.preventDefault();\n e.stopPropagation();\n if (state.isBusy) {\n log('Sync is already in progress.');\n return;\n }\n if (state.pathInfo) {\n log('Manual force sync triggered...');\n if(!hideUI && logArea) logArea.textContent = '';\n state.forceSync = true;\n startCheck(true);\n }\n };\n \n const init = () => {\n if (hideUI) {\n suneContainer.style.display = 'none';\n }\n worker = createWorker();\n worker.onmessage = handleWorkerMessage;\n \n if (mainSyncButton) {\n mainSyncButton.style.position = 'relative';\n const badge = document.createElement('span');\n badge.id = 'suneSyncBadge';\n badge.className = 'hidden absolute -top-1 -right-1 h-4 min-w-[1rem] px-1 rounded-full bg-indigo-600 text-white text-[10px] font-bold leading-4 flex items-center justify-center';\n mainSyncButton.appendChild(badge);\n }\n\n log(`GitHub Sync v1.4.0 ready for \"${activeSune.name}\".`);\n \n const pathInfo = parseGhPath(activeSune.url);\n if (pathInfo) {\n if (!hideUI) {\n const branchText = pathInfo.branch ? `@<span class=\"font-semibold\">${pathInfo.branch}</span>` : '';\n infoText.innerHTML = `Sync Target: <br><span class=\"font-mono text-xs bg-gray-200 px-1 py-0.5 rounded\">${pathInfo.owner}/${pathInfo.repo}${branchText}/${pathInfo.path}</span>`;\n }\n updateSyncButtonUI('none', null);\n startCheck(false);\n } else {\n if (!hideUI) {\n infoText.innerHTML = 'Set a `gh://owner/repo@branch/path.sune` URL in Sune settings (click ✺) to enable sync.';\n }\n updateSyncButtonUI('none', null);\n }\n\n if(!hideUI) syncBtn.addEventListener('click', performUpload);\n \n if (mainSyncButton) {\n if(mainSyncButton.__syncClickListener) {\n mainSyncButton.removeEventListener('click', mainSyncButton.__syncClickListener);\n }\n mainSyncButton.__syncClickListener = handleMainSyncClick;\n mainSyncButton.addEventListener('click', mainSyncButton.__syncClickListener);\n }\n\n if (window.lucide) window.lucide.createIcons();\n\n const observer = new MutationObserver((mutationsList) => {\n for (const mutation of mutationsList) {\n if (mutation.removedNodes) {\n for (const removedNode of mutation.removedNodes) {\n if (removedNode === suneContainer || removedNode.contains(suneContainer)) {\n observer.disconnect();\n cleanup();\n return;\n }\n }\n }\n }\n });\n if (suneContainer.parentElement) {\n observer.observe(suneContainer.parentElement, { childList: true });\n }\n };\n\n init();\n})();\n</script>\n",
"extension_html": ""
}
}
]
},
{
"name": "terminal.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/terminal.sune",
"sune": [
{
"id": "jc30des",
"name": "Terminal",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@main/terminal.sune",
"updatedAt": 1756931173984,
"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": "<div id=\"sune-terminal-container\" class=\"w-full max-w-4xl mx-auto my-4 font-mono text-sm\">\n <div class=\"flex flex-col bg-gray-900 text-gray-200 rounded-lg shadow-xl overflow-hidden border border-gray-700 h-72 md:h-96\">\n \n <!-- Toolbar -->\n <div class=\"flex items-center justify-between px-3 py-1.5 bg-gray-800/70 border-b border-gray-700\">\n <span class=\"text-xs text-gray-400\">JS Terminal</span>\n <button id=\"sune-terminal-clear\" title=\"Clear Console\" class=\"p-1 rounded text-gray-400 hover:bg-gray-700 hover:text-white transition-colors\">\n <i data-lucide=\"trash-2\" class=\"h-4 w-4\"></i>\n </button>\n </div>\n\n <!-- Output Area -->\n <div id=\"sune-terminal-output\" class=\"flex-1 p-2 overflow-y-auto\">\n <!-- Log entries will be injected here -->\n </div>\n\n <!-- Input Area -->\n <div class=\"flex items-center gap-2 p-2 border-t border-gray-700 bg-gray-800/70\">\n <span class=\"text-green-400\">&gt;</span>\n <form id=\"sune-terminal-form\" class=\"flex-1\">\n <input id=\"sune-terminal-input\" type=\"text\" class=\"w-full bg-transparent border-none outline-none focus:ring-0 placeholder:text-gray-500\" placeholder=\"Type a command and press Enter\" spellcheck=\"false\" autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"none\">\n </form>\n </div>\n </div>\n</div>\n\n<script>\n(function() {\n // Ensure we don't run this script multiple times if the sune is re-injected\n if (window.suneTerminalInitialized) {\n // A basic cleanup might be needed here in a more complex SPA,\n // but for now, we just prevent re-initialization.\n return;\n }\n window.suneTerminalInitialized = true;\n\n const suneId = window.SUNE ? window.SUNE.id : 'default-terminal';\n const historyKey = `sune_terminal_history_${suneId}`;\n\n const container = document.getElementById('sune-terminal-container');\n const output = document.getElementById('sune-terminal-output');\n const form = document.getElementById('sune-terminal-form');\n const input = document.getElementById('sune-terminal-input');\n const clearBtn = document.getElementById('sune-terminal-clear');\n\n let commandHistory = [];\n let historyIndex = -1;\n\n // --- Utility Functions ---\n\n const escapeHtml = (unsafe) => {\n return unsafe\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n };\n \n const scrollToBottom = () => {\n output.scrollTop = output.scrollHeight;\n };\n\n const formatValue = (value) => {\n if (typeof value === 'undefined') {\n return `<span class=\"text-gray-500\">undefined</span>`;\n }\n if (value === null) {\n return `<span class=\"text-purple-400\">null</span>`;\n }\n if (typeof value === 'string') {\n return `<span class=\"text-green-400\">'${escapeHtml(value)}'</span>`;\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return `<span class=\"text-purple-400\">${value}</span>`;\n }\n if (typeof value === 'function') {\n return `<span class=\"text-cyan-400 italic\">ƒ ${escapeHtml(value.name) || '(anonymous)'}()</span>`;\n }\n if (typeof value === 'object') {\n try {\n // Pretty print object with 2-space indentation\n const pretty = escapeHtml(JSON.stringify(value, null, 2));\n return `<pre class=\"!p-0 !bg-transparent !border-none\">${pretty}</pre>`;\n } catch (e) {\n return `<span class=\"text-red-400\">[Circular Object]</span>`;\n }\n }\n return escapeHtml(String(value));\n };\n \n // --- Core Logging Logic ---\n\n const logToTerminal = (content, type = 'log') => {\n const entry = document.createElement('div');\n entry.className = 'flex items-start gap-2 mb-1';\n\n let icon = '';\n let textColor = 'text-gray-300';\n\n switch (type) {\n case 'command':\n icon = `<span class=\"text-gray-500 mt-0.5\">&gt;</span>`;\n textColor = 'text-gray-100';\n break;\n case 'return':\n icon = `<span class=\"text-gray-500 mt-0.5\">&lt;</span>`;\n textColor = 'text-gray-400';\n break;\n case 'warn':\n icon = `<span class=\"text-yellow-400 mt-0.5\"><i data-lucide=\"alert-triangle\" class=\"h-4 w-4\"></i></span>`;\n textColor = 'text-yellow-400';\n break;\n case 'error':\n icon = `<span class=\"text-red-400 mt-0.5\"><i data-lucide=\"x-circle\" class=\"h-4 w-4\"></i></span>`;\n textColor = 'text-red-400';\n break;\n case 'info':\n icon = `<span class=\"text-blue-400 mt-0.5\"><i data-lucide=\"info\" class=\"h-4 w-4\"></i></span>`;\n textColor = 'text-blue-400';\n break;\n }\n \n entry.innerHTML = `${icon}<div class=\"flex-1 ${textColor}\">${content}</div>`;\n output.appendChild(entry);\n scrollToBottom();\n };\n\n // --- History Management ---\n\n const loadHistory = () => {\n try {\n const storedHistory = localStorage.getItem(historyKey);\n if (storedHistory) {\n commandHistory = JSON.parse(storedHistory);\n historyIndex = commandHistory.length;\n }\n } catch (e) {\n console.error(\"Failed to load command history:\", e);\n commandHistory = [];\n }\n };\n\n const saveHistory = () => {\n try {\n // Limit history size\n if (commandHistory.length > 100) {\n commandHistory.splice(0, commandHistory.length - 100);\n }\n localStorage.setItem(historyKey, JSON.stringify(commandHistory));\n } catch (e) {\n console.error(\"Failed to save command history:\", e);\n }\n };\n\n const addToHistory = (command) => {\n if (command && command !== commandHistory[commandHistory.length - 1]) {\n commandHistory.push(command);\n saveHistory();\n }\n historyIndex = commandHistory.length;\n };\n \n // --- Event Handlers ---\n \n form.addEventListener('submit', (e) => {\n e.preventDefault();\n const command = input.value.trim();\n \n if (command) {\n logToTerminal(escapeHtml(command), 'command');\n addToHistory(command);\n\n try {\n // Using new Function() is slightly safer than eval() as it runs in the global scope\n const result = (new Function(`return ${command}`))();\n if (typeof result !== 'undefined') {\n logToTerminal(formatValue(result), 'return');\n }\n } catch (error) {\n logToTerminal(escapeHtml(error.message), 'error');\n }\n }\n input.value = '';\n scrollToBottom();\n });\n\n input.addEventListener('keydown', (e) => {\n if (e.key === 'ArrowUp') {\n e.preventDefault();\n if (historyIndex > 0) {\n historyIndex--;\n input.value = commandHistory[historyIndex];\n input.setSelectionRange(input.value.length, input.value.length);\n }\n } else if (e.key === 'ArrowDown') {\n e.preventDefault();\n if (historyIndex < commandHistory.length - 1) {\n historyIndex++;\n input.value = commandHistory[historyIndex];\n input.setSelectionRange(input.value.length, input.value.length);\n } else {\n historyIndex = commandHistory.length;\n input.value = '';\n }\n } else if (e.key === 'l' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n output.innerHTML = '';\n logToTerminal('Console was cleared.', 'info');\n }\n });\n\n clearBtn.addEventListener('click', () => {\n output.innerHTML = '';\n logToTerminal('Console was cleared.', 'info');\n });\n\n // --- Console Interception ---\n \n const originalConsole = {};\n const consoleMethods = ['log', 'warn', 'error', 'info', 'debug'];\n\n consoleMethods.forEach(method => {\n originalConsole[method] = console[method];\n console[method] = (...args) => {\n // Call original console method\n originalConsole[method].apply(console, args);\n \n // Log to our sune terminal\n const formattedArgs = args.map(formatValue).join(' ');\n logToTerminal(formattedArgs, method);\n };\n });\n \n // --- Cleanup ---\n\n const observer = new MutationObserver((mutationsList) => {\n for (const mutation of mutationsList) {\n if (mutation.removedNodes) {\n mutation.removedNodes.forEach(node => {\n if (node === container) {\n // Restore original console methods when sune is removed\n consoleMethods.forEach(method => {\n console[method] = originalConsole[method];\n });\n observer.disconnect();\n window.suneTerminalInitialized = false;\n }\n });\n }\n }\n });\n\n if (container.parentElement) {\n observer.observe(container.parentElement, { childList: true });\n }\n\n // --- Initialization ---\n \n const init = () => {\n loadHistory();\n logToTerminal('Sune Terminal Initialized. Ctrl+L to clear.', 'info');\n lucide.createIcons(); // Render any new icons\n input.focus();\n };\n\n init();\n\n})();\n</script>\n",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />"
}
}
]
},
{
"name": "titles.sune",
"raw": "https://raw.githubusercontent.com/sune-org/store/main/titles.sune",
"sune": [
{
"id": "dn7bag8",
"name": "Auto Title",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@main/titles.sune",
"updatedAt": 1756929873067,
"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": "low",
"system_prompt": "",
"html": "<!-- Sune: Auto-Title Generator (v4 with Retry Logic) -->\n<div id=\"auto-title-sune-container\">\n <!-- Debugging UI. Change ENABLE_DEBUG to false to disable completely. -->\n <div id=\"auto-title-debug-panel\" class=\"hidden fixed bottom-0 left-0 right-0 z-[100] bg-gray-900/90 text-white backdrop-blur-sm shadow-2xl shadow-black/50 transition-all duration-300 max-h-0\">\n <div class=\"flex flex-col h-full max-h-[40vh]\">\n <div class=\"flex items-center justify-between p-2 border-b border-gray-700 bg-gray-800\">\n <h3 class=\"text-sm font-semibold tracking-wider\">Auto-Title Sune Log</h3>\n <div class=\"flex items-center gap-2\">\n <button id=\"auto-title-manual-trigger\" title=\"Generate Title Manually\" class=\"p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md\">\n <i data-lucide=\"wand-2\" class=\"h-4 w-4\"></i>\n </button>\n <button id=\"auto-title-clear-log\" title=\"Clear Log\" class=\"p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md\">\n <i data-lucide=\"trash-2\" class=\"h-4 w-4\"></i>\n </button>\n <button id=\"auto-title-toggle-panel\" title=\"Minimize\" class=\"p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md\">\n <i data-lucide=\"minus\" class=\"h-4 w-4\"></i>\n </button>\n </div>\n </div>\n <pre id=\"auto-title-log-content\" class=\"flex-1 p-3 text-xs font-mono overflow-y-auto text-gray-300 leading-snug\"></pre>\n </div>\n </div>\n <button id=\"auto-title-debug-fab\" class=\"hidden fixed bottom-4 right-4 z-[99] h-10 w-10 bg-indigo-600 text-white rounded-full flex items-center justify-center shadow-lg hover:bg-indigo-700 active:scale-95 transition-all\">\n <i data-lucide=\"file-terminal\" class=\"h-5 w-5\"></i>\n </button>\n</div>\n\n<script>\n(function() {\n \"use strict\";\n\n // --- CONFIGURATION ---\n const ENABLE_DEBUG = true;\n const MAX_RETRIES = 20; // Number of times to check for the message\n const RETRY_DELAY_MS = 50; // Milliseconds to wait between retries\n \n // --- CONSTANTS ---\n const CHAR_LIMIT = 120000;\n const TITLE_SYSTEM_PROMPT = `You are a title generation expert. Your task is to create a short, concise, and descriptive title for a chat thread based on the user's first message. Rules: The title must be 8 words or less. The title should accurately summarize the main topic or question. Do NOT use quotation marks. Respond ONLY with the generated title.`;\n\n // --- SUNE CONTEXT & DOM ---\n const suneId = window.SUNE.id;\n window.doCreateTitle = false // keep this & the comment\n const suneContainer = document.getElementById('auto-title-sune-container');\n if (!suneContainer) return;\n \n const debugPanel = suneContainer.querySelector('#auto-title-debug-panel');\n const logContent = suneContainer.querySelector('#auto-title-log-content');\n const toggleButton = suneContainer.querySelector('#auto-title-toggle-panel');\n const clearButton = suneContainer.querySelector('#auto-title-clear-log');\n const fab = suneContainer.querySelector('#auto-title-debug-fab');\n const manualTriggerButton = suneContainer.querySelector('#auto-title-manual-trigger');\n\n // --- UTILITY FUNCTIONS ---\n const log = (message, data = null) => {\n if (!ENABLE_DEBUG) return;\n console.log(`[AutoTitleSune]`, message, data !== null ? data : '');\n if (!logContent) return;\n const timestamp = new Date().toLocaleTimeString();\n const line = document.createElement('div');\n line.innerHTML = `<span class=\"text-gray-500\">${timestamp}:</span> <span class=\"text-gray-100\">${escapeHtml(String(message))}</span>`;\n if (data) {\n const dataPre = document.createElement('pre');\n dataPre.className = 'mt-1 p-2 bg-black/50 rounded text-sky-300 text-[11px] overflow-x-auto';\n dataPre.textContent = JSON.stringify(data, null, 2);\n line.appendChild(dataPre);\n }\n logContent.appendChild(line);\n logContent.scrollTop = logContent.scrollHeight;\n };\n const escapeHtml = (unsafe) => unsafe ? unsafe.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\").replace(/'/g, \"&#039;\") : '';\n const partsToText = (parts) => Array.isArray(parts) ? parts.filter(p => p.type === 'text').map(p => p.text).join('\\n') : '';\n\n /**\n * The core logic for generating and setting a thread title. We know this works.\n * @param {string} threadId The ID of the thread to process.\n * @param {string} triggerType A label for logging ('Automatic' or 'Manual').\n */\n const generateAndSetTitle = async (threadId, triggerType) => {\n try {\n log(`Starting title generation for thread ${threadId} (Trigger: ${triggerType})`);\n\n const thread = window.SUNE.getThread(threadId);\n if (!thread) throw new Error(`Could not retrieve thread object for id: ${threadId}`);\n\n const firstUserMessage = thread.messages.find(m => m.role === 'user');\n if (!firstUserMessage || !firstUserMessage.content) {\n throw new Error('No user message object found in the retrieved thread.');\n }\n \n let userText = partsToText(firstUserMessage.content).trim();\n if (!userText) {\n userText = firstUserMessage.content.some(p => p.type !== 'text') ? '(Message with an attachment)' : '';\n }\n if (!userText) throw new Error('User message has no text or attachments. Aborting.');\n\n const truncatedText = userText.substring(0, CHAR_LIMIT);\n log(`Extracted user content (${truncatedText.length} chars).`);\n\n const model = window.SUNE.titleModel;\n const apiKey = model.startsWith('oai:') ? window.SUNE.apiKeyOAI : window.SUNE.apiKeyOR;\n\n if (!model) throw new Error('Title generation model (SUNE.titleModel) is not set.');\n if (!apiKey) throw new Error('API key for the selected provider is not set.');\n\n const payload = { model: model.replace(/^(or:|oai:)/, ''), messages: [{ role: 'system', content: TITLE_SYSTEM_PROMPT }, { role: 'user', content: truncatedText }], max_tokens: 20, temperature: 0.5, stream: false };\n log('Sending API request...', payload);\n \n const response = await fetch(\"https://openrouter.ai/api/v1/chat/completions\", { method: \"POST\", headers: { \"Authorization\": `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" }, body: JSON.stringify(payload) });\n if (!response.ok) {\n throw new Error(`API request failed with status ${response.status}: ${await response.text()}`);\n }\n\n const result = await response.json();\n log('Received API response:', result);\n const newTitle = result.choices?.[0]?.message?.content?.trim().replace(/[\"']/g, '');\n if (!newTitle) throw new Error('Could not extract a valid title from the API response.');\n \n log(`Generated title: \"${newTitle}\"`);\n await window.SUNE.setThreadTitle(threadId, newTitle);\n log(`Successfully set title for thread ${threadId}.`);\n\n } catch (error) {\n log(`An error occurred:`, error.message);\n console.error(error);\n }\n };\n \n /**\n * NEW: Robustly finds the first user message using polling/retries.\n * @param {string} threadId The ID of the thread to process.\n */\n const processThreadWithRetry = async (threadId) => {\n for (let i = 0; i < MAX_RETRIES; i++) {\n log(`Attempt ${i + 1}/${MAX_RETRIES} to find user message in thread ${threadId}...`);\n const thread = window.SUNE.getThread(threadId);\n const userMessageExists = thread?.messages?.some(m => m.role === 'user');\n\n if (userMessageExists) {\n log(`Success! User message found on attempt ${i + 1}.`);\n await generateAndSetTitle(threadId, 'Automatic');\n return; // Exit the retry loop\n }\n\n // If not found, wait before the next attempt\n await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));\n }\n log(`Error: Max retries (${MAX_RETRIES}) reached. Could not find user message in thread ${threadId}. Aborting automatic title generation.`);\n };\n\n // --- EVENT HANDLERS ---\n const handleAutomaticTrigger = (event) => {\n if (window.SUNE.id !== suneId) return;\n const { threadId } = event.detail;\n if (!threadId) {\n log('Error: new-thread event received without a threadId.');\n return;\n }\n // Instead of calling the main logic directly, we call the new retry wrapper.\n processThreadWithRetry(threadId);\n };\n \n const handleManualTrigger = () => {\n const currentThreadId = window.state.currentThreadId;\n if (!currentThreadId) {\n log('Manual Trigger Error: No active thread selected.');\n alert('No active thread. Please start a conversation first.');\n return;\n }\n // Manual trigger can still call the function directly as the state is guaranteed to be ready.\n generateAndSetTitle(currentThreadId, 'Manual');\n };\n\n // --- INITIALIZATION ---\n const initialize = () => {\n if (!suneContainer) return;\n if (ENABLE_DEBUG) {\n fab.classList.remove('hidden');\n lucide.createIcons({ nodes: suneContainer.querySelectorAll('[data-lucide]') });\n const showPanel = () => { fab.classList.add('hidden'); debugPanel.classList.remove('hidden'); setTimeout(() => { debugPanel.classList.add('max-h-[40vh]'); debugPanel.classList.remove('max-h-0'); }, 10); };\n const hidePanel = () => { debugPanel.classList.add('max-h-0'); debugPanel.classList.remove('max-h-[40vh]'); fab.classList.remove('hidden'); setTimeout(() => { if(debugPanel.classList.contains('max-h-0')) debugPanel.classList.add('hidden'); }, 300); };\n fab.addEventListener('click', showPanel);\n toggleButton.addEventListener('click', hidePanel);\n clearButton.addEventListener('click', () => { if(logContent) logContent.innerHTML = ''; log('Log cleared.'); });\n manualTriggerButton.addEventListener('click', handleManualTrigger);\n }\n\n document.addEventListener('sune:new-thread', handleAutomaticTrigger);\n log('Auto-Title Sune (v4 with Retry Logic) initialized.');\n };\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initialize);\n } else {\n initialize();\n }\n})();\n</script>\n",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />"
}
}
]
}
]