import { initNDKPage, getPubkey, injectHeaderAvatar, subscribe, publishEvent, disconnect, getVersion, updateVersionDisplay, getUserSettings, patchUserSettings, onUserSettings } from './js/init-ndk.mjs'; import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs"; import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs'; import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs'; import { initAiSectionWithLocalConfig, getDefaultAiConfig, loadAiConfigLocal, saveAiConfigLocal, getProviderEntries, mergeAiConfigFromSettings, normalizeAiConfig, normalizeBaseUrl } from './js/ai-ui.mjs'; const DIDACTYL_TOOLS = [ ['nostr_post', 'Publish a Nostr event to connected relays'], ['nostr_delete', 'Request deletion of one or more previously published events'], ['nostr_react', 'React to a Nostr event with like/dislike/emoji'], ['nostr_query', 'Query events from relays using a Nostr filter'], ['nostr_my_events', 'Query recent events authored by this agent'], ['nostr_dm_send', 'Send a NIP-04 encrypted DM'], ['nostr_dm_send_nip17', 'Send a private DM using NIP-17 gift wrap protocol'], ['nostr_profile_get', 'Look up a Nostr profile by pubkey'], ['nostr_nip05_lookup', 'Look up or verify a NIP-05 identifier'], ['nostr_encode', 'Encode a Nostr entity into nostr: URI'], ['nostr_decode', 'Decode a Nostr bech32/nostr: URI'], ['nostr_relay_status', 'Get connection status and statistics for all relays'], ['nostr_relay_info', 'Fetch NIP-11 relay information document'], ['nostr_subscription_status', 'List currently managed runtime Nostr subscriptions'], ['nostr_subscription_set', 'Update one managed runtime subscription by name'], ['nostr_encrypt', 'Encrypt plaintext using NIP-44 for a recipient'], ['nostr_decrypt', 'Decrypt NIP-44 ciphertext from a sender'], ['nostr_list_manage', 'Add/remove tag tuples in replaceable list events'], ['nostr_block_list', 'View blocked pubkeys, event IDs, or hashtags'], ['nostr_block_edit', 'Add or remove blocked tuples in kind-10000 block list'], ['nostr_pubkey', 'Return this agent pubkey in hex format'], ['nostr_npub', 'Return this agent pubkey encoded as npub'], ['my_pubkey', 'Alias for nostr_pubkey'], ['my_npub', 'Alias for nostr_npub'], ['agent_identity', 'Build agent identity context block'], ['admin_identity', 'Build admin identity context block'], ['nostr_admin_profile', 'Build admin profile context block'], ['nostr_admin_contacts', 'Build admin contacts context block'], ['nostr_admin_relays', 'Build admin relay context block'], ['nostr_admin_notes', 'Build admin notes context block'], ['nostr_agent_profile', 'Build agent profile context block'], ['nostr_agent_contacts', 'Build agent contacts context block'], ['nostr_agent_relays', 'Build agent relay context block'], ['nostr_agent_notes', 'Build agent notes context block'], ['my_kind0_profile', 'Alias for nostr_agent_profile'], ['my_contacts', 'Alias for nostr_agent_contacts'], ['my_relays', 'Alias for nostr_agent_relays'], ['my_notes', 'Alias for nostr_agent_notes'], ['skill_create', 'Create or update a skill definition'], ['skill_edit', 'Edit an existing self skill by d tag'], ['skill_list', 'List available skills discovered online'], ['skill_adopt', 'Adopt a skill by adding its address to kind 10123'], ['skill_remove', 'Remove a skill address from kind 10123'], ['skill_search', 'Search public skills by query/author'], ['trigger_list', 'List active triggered skills and runtime status'], ['task_list', 'Build current task list context block'], ['task_manage', 'Manage short-term task memory'], ['memory_save', 'Prepend a new entry to encrypted agent memory'], ['memory_recall', 'Recall encrypted agent memory'], ['model_get', 'Get current active LLM runtime configuration'], ['model_set', 'Update active LLM configuration'], ['model_list', 'List available model IDs'], ['config_store', 'Encrypt and publish agent config'], ['config_recall', 'Fetch and decrypt agent config'], ['agent_version', 'Return current Didactyl version'], ['local_http_fetch', 'Fetch HTTP(S) resources'], ['local_shell_exec', 'Execute a shell command'], ['local_file_read', 'Read a local file as text'], ['local_file_write', 'Write text content to a local file'], ['tool_list', 'List available tools with schemas'], ['cashu_wallet_balance', 'Return wallet balances by mint and unit'], ['cashu_wallet_info', 'Fetch mint info'], ['cashu_wallet_mint_quote', 'Request a mint quote'], ['cashu_wallet_mint_check', 'Check whether a mint quote is paid/issued'], ['cashu_wallet_mint_claim', 'Claim minted proofs'], ['cashu_wallet_melt_quote', 'Request a melt quote'], ['cashu_wallet_melt_pay', 'Pay a melt quote'], ['cashu_wallet_check_proofs', 'Ask mint for current proof states'], ['cashu_wallet_receive_token', 'Receive an ecash token string'], ['cashu_wallet_send_token', 'Create an outbound ecash token'], ['cashu_wallet_mints_set', 'Set wallet/public mints'], ['blossom_upload', 'Upload a local file to a Blossom server'], ['blossom_download', 'Download a Blossom blob to local path'], ['blossom_head', 'Fetch Blossom blob metadata by SHA-256'], ['blossom_delete', 'Delete a Blossom blob by SHA-256'], ['blossom_list', 'List Blossom blobs for a pubkey'], ['nostr_post_readme', 'Publish README.md as kind 30023'], ['nostr_file_md_to_longform_post', 'Read markdown and publish as kind 30023'] ].map(([name, description]) => ({ name, description })); const TOOL_NAME_SET = new Set(DIDACTYL_TOOLS.map(t => t.name)); const versionInfo = await getVersion(); const VERSION = versionInfo.VERSION; console.log(`[skills-edit.html ${VERSION}] Loading...`); const AI_STORAGE_KEY = 'ai_chat_openai_config_v1'; let updateIntervalId = null; let currentPubkey = null; let isAuthenticated = false; let authMode = 'required'; let authedPageInitialized = false; let relayActivityListenersBound = false; let hamburgerInstance = null; let isNavOpen = false; let logoutHamburger = null; let themeToggleHamburger = null; let isDarkMode = false; let pageSettings = {}; let unsubscribeUserSettings = null; let activeTab = 'my'; let selectedSkillAddress = null; let skillsByAddress = new Map(); let mySkillSub = null; let mySkillSubId = null; let publicSkillSub = null; let publicSkillSubId = null; let adoptionSub = null; let adoptionSubId = null; let publicLoaded = false; let adoptedAddresses = []; let adoptionEventId = null; let requiresTools = []; let optionalTools = []; let requiresSkills = []; let aiConfig = loadAiConfigLocal(getDefaultAiConfig(), AI_STORAGE_KEY); let fetchedModels = []; const divBody = document.getElementById('divBody'); const divSideNav = document.getElementById('divSideNav'); const divFooterCenter = document.getElementById('divFooterCenter'); const divFooterRight = document.getElementById('divFooterRight'); const inpSkillSearch = document.getElementById('inpSkillSearch'); const btnTabMy = document.getElementById('btnTabMy'); const btnTabPublic = document.getElementById('btnTabPublic'); const btnTabAdopted = document.getElementById('btnTabAdopted'); const btnRefreshSkills = document.getElementById('btnRefreshSkills'); const btnNewSkill = document.getElementById('btnNewSkill'); const divSkillsList = document.getElementById('divSkillsList'); const divAdoptedList = document.getElementById('divAdoptedList'); const selSkillKind = document.getElementById('selSkillKind'); const inpSkillSlug = document.getElementById('inpSkillSlug'); const inpSkillDescription = document.getElementById('inpSkillDescription'); const selSkillTrigger = document.getElementById('selSkillTrigger'); const inpSkillFilter = document.getElementById('inpSkillFilter'); const inpSkillLlm = document.getElementById('inpSkillLlm'); const inpSkillTemperature = document.getElementById('inpSkillTemperature'); const inpSkillMaxTokens = document.getElementById('inpSkillMaxTokens'); const inpSkillSeed = document.getElementById('inpSkillSeed'); const inpRequiresSkill = document.getElementById('inpRequiresSkill'); const btnAddRequiresSkill = document.getElementById('btnAddRequiresSkill'); const preSkillHighlight = document.getElementById('preSkillHighlight'); const taSkillContent = document.getElementById('taSkillContent'); const btnSavePublic = document.getElementById('btnSavePublic'); const btnSavePrivate = document.getElementById('btnSavePrivate'); const btnCopyFork = document.getElementById('btnCopyFork'); const btnDeleteSkill = document.getElementById('btnDeleteSkill'); const btnAdoptCurrent = document.getElementById('btnAdoptCurrent'); const btnRemoveAdoptCurrent = document.getElementById('btnRemoveAdoptCurrent'); const btnPublishAdoption = document.getElementById('btnPublishAdoption'); const divEditorStatus = document.getElementById('divEditorStatus'); const inpToolSearch = document.getElementById('inpToolSearch'); const divToolsList = document.getElementById('divToolsList'); const divTriggerFilterHelper = document.getElementById('divTriggerFilterHelper'); const selLlmProvider = document.getElementById('selLlmProvider'); const inpLlmModelFilter = document.getElementById('inpLlmModelFilter'); const selLlmModel = document.getElementById('selLlmModel'); const btnLlmFavorite = document.getElementById('btnLlmFavorite'); const btnLlmApply = document.getElementById('btnLlmApply'); const divLlmHelperStatus = document.getElementById('divLlmHelperStatus'); const CRON_PRESETS = [ ['Every 5 min', '*/5 * * * *'], ['Hourly (:00)', '0 * * * *'], ['Daily noon', '0 12 * * *'], ['Daily midnight', '0 0 * * *'], ['Weekdays 9am', '0 9 * * 1-5'], ['Mondays 9am', '0 9 * * 1'] ]; const NOSTR_FILTER_PRESETS = [ ['Mentions admin (kind 1)', '{"#p":[""],"kinds":[1]}'], ['Notes only (kind 1, limit 20)', '{"kinds":[1],"limit":20}'], ['Profile updates (kind 0)', '{"kinds":[0]}'] ]; const WEBHOOK_FILTER_PRESETS = [ ['Match all', '{}'], ['POST only', '{"method":"POST"}'], ['Path /webhook', '{"path":"/webhook"}'] ]; function setStatus(msg) { divEditorStatus.textContent = String(msg || ''); } function setLlmHelperStatus(msg) { if (divLlmHelperStatus) divLlmHelperStatus.textContent = String(msg || ''); } function shortPubkey(pubkey) { const p = String(pubkey || ''); if (p.length < 14) return p; return `${p.slice(0, 6)}...${p.slice(-4)}`; } function normalizeSlug(raw) { return String(raw || '') .trim() .toLowerCase() .replace(/[^a-z0-9-_]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 64); } function escapeHtml(s) { const span = document.createElement('span'); span.textContent = String(s || ''); return span.innerHTML; } function getTagValue(tags, key) { const t = (Array.isArray(tags) ? tags : []).find(x => Array.isArray(x) && x[0] === key); return t && t[1] ? String(t[1]) : ''; } function getTagValues(tags, key) { return (Array.isArray(tags) ? tags : []) .filter(x => Array.isArray(x) && x[0] === key && typeof x[1] === 'string') .map(x => String(x[1])); } function getLlmProviderEntries() { return getProviderEntries(aiConfig); } function parseLlmToken(value) { const raw = String(value || '').trim(); const first = raw.split(',')[0].trim(); if (!first) return { provider: '', model: '' }; const slash = first.indexOf('/'); if (slash > 0 && slash < first.length - 1) { return { provider: first.slice(0, slash).trim(), model: first.slice(slash + 1).trim() }; } return { provider: String(aiConfig?.provider || '').trim(), model: first }; } function getOrderedModels(filterText = '') { const models = Array.isArray(fetchedModels) ? fetchedModels : []; const favorites = Array.isArray(aiConfig?.favorites) ? aiConfig.favorites : []; const q = String(filterText || '').trim().toLowerCase(); const filtered = models.filter((m) => { const id = String(m?.id || '').trim(); if (!id) return false; return !q || id.toLowerCase().includes(q); }); const fav = filtered.filter((m) => favorites.includes(String(m.id || ''))); const other = filtered.filter((m) => !favorites.includes(String(m.id || ''))); return { fav, other }; } function renderLlmProviderOptions() { if (!selLlmProvider) return; const providers = getLlmProviderEntries(); const current = String(aiConfig?.provider || '').trim(); if (!providers.length) { selLlmProvider.innerHTML = ''; return; } selLlmProvider.innerHTML = providers .map((p) => ``) .join(''); if (current) selLlmProvider.value = current; } function renderLlmModelOptions() { if (!selLlmModel) return; const current = String(parseLlmToken(inpSkillLlm?.value).model || aiConfig?.model || '').trim(); const { fav, other } = getOrderedModels(inpLlmModelFilter?.value || ''); const rows = []; for (const m of fav) { const id = String(m?.id || '').trim(); rows.push(``); } for (const m of other) { const id = String(m?.id || '').trim(); rows.push(``); } selLlmModel.innerHTML = rows.join('') || ''; if (current) selLlmModel.value = current; const selected = String(selLlmModel.value || '').trim(); const isFav = selected && Array.isArray(aiConfig?.favorites) && aiConfig.favorites.includes(selected); if (btnLlmFavorite) btnLlmFavorite.textContent = isFav ? '★ Favorited' : '☆ Favorite'; } function selectLlmProvider(providerName) { const name = String(providerName || '').trim(); if (!name) return; const providers = getLlmProviderEntries(); const selected = providers.find((p) => p.name === name); if (!selected) return; aiConfig = { ...aiConfig, provider: selected.name, base_url: selected.url, api_key: selected.api_key, credit_id: selected.credit_id, model: selected.model || aiConfig.model, max_tokens: Number.isFinite(selected.max_tokens) && selected.max_tokens > 0 ? Math.floor(selected.max_tokens) : aiConfig.max_tokens, temperature: Number.isFinite(selected.temperature) ? selected.temperature : aiConfig.temperature }; aiConfig = saveAiConfigLocal(aiConfig, AI_STORAGE_KEY); patchUserSettings({ global_llm: normalizeAiConfig(aiConfig) }).catch((error) => { console.warn('[skills-edit.html] failed to persist provider to user-settings:', error); }); renderLlmProviderOptions(); fetchLlmModels(); } async function fetchLlmModels() { const baseUrl = normalizeBaseUrl(aiConfig?.base_url || ''); const apiKey = String(aiConfig?.api_key || '').trim(); if (!baseUrl) { fetchedModels = []; renderLlmModelOptions(); setLlmHelperStatus('No provider base URL configured in AI settings.'); return; } if (!apiKey) { fetchedModels = []; renderLlmModelOptions(); setLlmHelperStatus('No API key configured for selected provider.'); return; } try { setLlmHelperStatus('Loading models...'); const response = await fetch(`${baseUrl}/v1/models`, { headers: { Authorization: `Bearer ${apiKey}` } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); const seen = new Set(); fetchedModels = (Array.isArray(data?.data) ? data.data : []) .map((m) => ({ id: String(m?.id || '').trim() })) .filter((m) => m.id && !seen.has(m.id) && seen.add(m.id)) .sort((a, b) => a.id.localeCompare(b.id)); renderLlmModelOptions(); setLlmHelperStatus(`Loaded ${fetchedModels.length} models from ${baseUrl}.`); } catch (error) { fetchedModels = []; renderLlmModelOptions(); setLlmHelperStatus(`Model fetch failed: ${String(error?.message || error)}`); } } function syncLlmHelperFromSkillInput() { if (!selLlmProvider || !selLlmModel) return; const parsed = parseLlmToken(inpSkillLlm?.value || ''); if (parsed.provider && Array.from(selLlmProvider.options).some((o) => o.value === parsed.provider)) { selLlmProvider.value = parsed.provider; } if (parsed.model && Array.from(selLlmModel.options).some((o) => o.value === parsed.model)) { selLlmModel.value = parsed.model; } renderLlmModelOptions(); } function applyLlmSelectionToInput() { const provider = String(selLlmProvider?.value || aiConfig?.provider || '').trim(); const model = String(selLlmModel?.value || '').trim(); if (!provider || !model) { setStatus('Select provider and model first.'); return; } inpSkillLlm.value = `${provider}/${model}`; setStatus(`Set llm tag to ${inpSkillLlm.value}`); syncLlmHelperFromSkillInput(); } function toggleCurrentModelFavorite() { const model = String(selLlmModel?.value || '').trim(); if (!model) return; const favorites = Array.isArray(aiConfig?.favorites) ? [...aiConfig.favorites] : []; const idx = favorites.indexOf(model); if (idx >= 0) favorites.splice(idx, 1); else favorites.push(model); aiConfig = { ...aiConfig, favorites }; aiConfig = saveAiConfigLocal(aiConfig, AI_STORAGE_KEY); patchUserSettings({ global_llm: normalizeAiConfig(aiConfig) }).catch((error) => { console.warn('[skills-edit.html] failed to persist favorites to user-settings:', error); }); renderLlmModelOptions(); } async function initializeLlmHelperFromSettings() { aiConfig = loadAiConfigLocal(aiConfig || getDefaultAiConfig(), AI_STORAGE_KEY); const llmNode = pageSettings?.global_llm || pageSettings?.ai || null; if (llmNode && typeof llmNode === 'object') { aiConfig = mergeAiConfigFromSettings(aiConfig, llmNode); } aiConfig = saveAiConfigLocal(aiConfig, AI_STORAGE_KEY); renderLlmProviderOptions(); renderLlmModelOptions(); syncLlmHelperFromSkillInput(); fetchLlmModels(); } function parseAddress(address) { const parts = String(address || '').split(':'); if (parts.length < 3) return null; const kind = Number(parts[0]); const pubkey = parts[1] || ''; const dTag = parts.slice(2).join(':'); if (!Number.isFinite(kind) || !pubkey || !dTag) return null; return { kind, pubkey, dTag }; } function makeAddress(kind, pubkey, dTag) { return `${Number(kind)}:${String(pubkey || '').trim()}:${String(dTag || '').trim()}`; } function isAddressAdopted(address) { return adoptedAddresses.includes(String(address || '')); } function cronPartsFromFilter(filterRaw) { const parts = String(filterRaw || '').trim().split(/\s+/); if (parts.length === 5) return parts; return ['*', '*', '*', '*', '*']; } function dmFromFromFilter(filterRaw) { try { const parsed = JSON.parse(String(filterRaw || '{}')); const from = String(parsed?.from || 'admin'); if (['admin', 'wot', 'any'].includes(from)) return from; } catch (_error) {} return 'admin'; } function setFilterPlaceholderByTrigger(trigger) { const t = String(trigger || '').trim(); if (!t) { inpSkillFilter.placeholder = '(select a trigger)'; return; } if (t === 'dm') inpSkillFilter.placeholder = '{"from":"admin"}'; else if (t === 'cron') inpSkillFilter.placeholder = '0 12 * * *'; else if (t === 'nostr-subscription') inpSkillFilter.placeholder = '{"#p":[""],"kinds":[1]}'; else if (t === 'webhook') inpSkillFilter.placeholder = '{}'; else if (t === 'chain') inpSkillFilter.placeholder = 'source-skill-d-tag'; else inpSkillFilter.placeholder = 'type-specific filter'; } function renderTriggerFilterHelper() { if (!divTriggerFilterHelper) return; const trigger = String(selSkillTrigger.value || '').trim(); setFilterPlaceholderByTrigger(trigger); if (!trigger) { divTriggerFilterHelper.innerHTML = '
Select a trigger to see filter options and presets.
'; return; } if (trigger === 'dm') { const currentFrom = dmFromFromFilter(inpSkillFilter.value); divTriggerFilterHelper.innerHTML = `
DM filter helper
Writes JSON like {"from":"admin"}.
`; const sel = document.getElementById('selDmFilterFrom'); const btn = document.getElementById('btnApplyDmFilter'); if (btn && sel) { btn.addEventListener('click', () => { inpSkillFilter.value = JSON.stringify({ from: String(sel.value || 'admin') }); setStatus(`Set dm filter: ${inpSkillFilter.value}`); }); } return; } if (trigger === 'cron') { const parts = cronPartsFromFilter(inpSkillFilter.value); divTriggerFilterHelper.innerHTML = `
Cron filter helper
Format: minute hour day-of-month month day-of-week
`; const btnPreset = document.getElementById('btnApplyCronPreset'); const selPreset = document.getElementById('selCronPreset'); const btnCompose = document.getElementById('btnComposeCron'); if (btnPreset && selPreset) { btnPreset.addEventListener('click', () => { inpSkillFilter.value = String(selPreset.value || '').trim(); setStatus(`Set cron filter: ${inpSkillFilter.value}`); renderTriggerFilterHelper(); }); } if (btnCompose) { btnCompose.addEventListener('click', () => { const min = String(document.getElementById('inpCronMin')?.value || '*').trim() || '*'; const hour = String(document.getElementById('inpCronHour')?.value || '*').trim() || '*'; const dom = String(document.getElementById('inpCronDom')?.value || '*').trim() || '*'; const mon = String(document.getElementById('inpCronMon')?.value || '*').trim() || '*'; const dow = String(document.getElementById('inpCronDow')?.value || '*').trim() || '*'; inpSkillFilter.value = `${min} ${hour} ${dom} ${mon} ${dow}`; setStatus(`Set cron filter: ${inpSkillFilter.value}`); }); } return; } if (trigger === 'chain') { const options = Array.from(skillsByAddress.values()) .filter((s) => s && s.slug) .sort((a, b) => String(a.slug).localeCompare(String(b.slug))); divTriggerFilterHelper.innerHTML = `
Chain filter helper
Filter should be the source skill d tag.
`; const selChain = document.getElementById('selChainSource'); const btnChain = document.getElementById('btnApplyChain'); if (selChain && inpSkillFilter.value) selChain.value = String(inpSkillFilter.value || '').trim(); if (btnChain && selChain) { btnChain.addEventListener('click', () => { const v = String(selChain.value || '').trim(); if (!v) return; inpSkillFilter.value = v; setStatus(`Set chain filter: ${inpSkillFilter.value}`); }); } return; } if (trigger === 'nostr-subscription') { divTriggerFilterHelper.innerHTML = `
Nostr-subscription filter helper
Filter is a JSON-encoded Nostr subscription filter.
`; const sel = document.getElementById('selNostrFilterPreset'); const btn = document.getElementById('btnApplyNostrFilter'); if (btn && sel) { btn.addEventListener('click', () => { inpSkillFilter.value = String(sel.value || '').trim(); setStatus('Set nostr-subscription filter preset.'); }); } return; } if (trigger === 'webhook') { divTriggerFilterHelper.innerHTML = `
Webhook filter helper
Use {} to match all requests.
`; const sel = document.getElementById('selWebhookFilterPreset'); const btn = document.getElementById('btnApplyWebhookFilter'); if (btn && sel) { btn.addEventListener('click', () => { inpSkillFilter.value = String(sel.value || '{}').trim(); setStatus(`Set webhook filter: ${inpSkillFilter.value}`); }); } return; } divTriggerFilterHelper.innerHTML = '
No helper available for this trigger.
'; } function toggleTabUI() { btnTabMy.classList.toggle('active', activeTab === 'my'); btnTabPublic.classList.toggle('active', activeTab === 'public'); btnTabAdopted.classList.toggle('active', activeTab === 'adopted'); } function clearEditor() { selectedSkillAddress = null; selSkillKind.value = '31123'; inpSkillSlug.value = ''; inpSkillDescription.value = ''; selSkillTrigger.value = ''; inpSkillFilter.value = ''; inpSkillLlm.value = ''; inpSkillTemperature.value = ''; inpSkillMaxTokens.value = ''; inpSkillSeed.value = ''; taSkillContent.value = '## Purpose\nYou are a helpful assistant.\n\nInput:\n{{message}}'; requiresTools = []; optionalTools = []; requiresSkills = []; renderTagChips(); renderHighlight(); renderTools(); renderTriggerFilterHelper(); syncLlmHelperFromSkillInput(); setStatus('New skill ready.'); btnCopyFork.style.display = 'none'; } function renderTagChips() { // Tag chips panel intentionally removed from UI to save space. // Keep this as a no-op so existing workflows can continue calling it safely. } function getVisibleSkills() { const q = String(inpSkillSearch.value || '').trim().toLowerCase(); const skills = Array.from(skillsByAddress.values()); let scoped = []; if (activeTab === 'my') { scoped = skills.filter(s => s.author === currentPubkey); } else if (activeTab === 'public') { scoped = skills.filter(s => s.kind === 31123 && s.author !== currentPubkey); } else { scoped = adoptedAddresses .map(addr => skillsByAddress.get(addr) || { address: addr, slug: parseAddress(addr)?.dTag || addr, description: '(not loaded yet)', author: parseAddress(addr)?.pubkey || '', kind: parseAddress(addr)?.kind || 31123, missing: true, created_at: 0 }); } if (q) { scoped = scoped.filter((s) => { const text = `${s.slug || ''} ${s.description || ''} ${s.author || ''}`.toLowerCase(); return text.includes(q); }); } if (activeTab !== 'adopted') { scoped.sort((a, b) => { const ta = Number(a.created_at || 0); const tb = Number(b.created_at || 0); if (tb !== ta) return tb - ta; return String(a.slug || '').localeCompare(String(b.slug || '')); }); } return scoped; } function renderSkillsList() { toggleTabUI(); const list = getVisibleSkills(); if (!list.length) { divSkillsList.innerHTML = '
No skills in this tab.
'; return; } divSkillsList.innerHTML = list.map((s) => { const adopted = isAddressAdopted(s.address); const active = selectedSkillAddress === s.address ? 'active' : ''; const author = shortPubkey(s.author); return `
${escapeHtml(s.slug || '(missing slug)')} ${adopted ? '' : ''}
${escapeHtml(s.description || '')}
${escapeHtml(author)} · ${escapeHtml(String(s.kind || ''))}${s.missing ? ' · missing' : ''}
`; }).join(''); Array.from(divSkillsList.querySelectorAll('.skillItem[data-address]')).forEach((el) => { el.addEventListener('click', () => { const address = String(el.getAttribute('data-address') || ''); loadSkillIntoEditor(address); }); }); } function renderAdoptedList() { if (!adoptedAddresses.length) { divAdoptedList.innerHTML = '
No adopted skills yet.
'; return; } divAdoptedList.innerHTML = adoptedAddresses.map((addr, i) => { const s = skillsByAddress.get(addr); const label = s ? `${s.slug} (${shortPubkey(s.author)})` : (parseAddress(addr)?.dTag || addr); return `
${escapeHtml(label)}
`; }).join(''); Array.from(divAdoptedList.querySelectorAll('button[data-act][data-i]')).forEach((btn) => { btn.addEventListener('click', () => { const i = Number(btn.getAttribute('data-i')); const act = String(btn.getAttribute('data-act') || ''); if (!Number.isFinite(i) || i < 0 || i >= adoptedAddresses.length) return; if (act === 'remove') { adoptedAddresses.splice(i, 1); } else if (act === 'up' && i > 0) { const tmp = adoptedAddresses[i - 1]; adoptedAddresses[i - 1] = adoptedAddresses[i]; adoptedAddresses[i] = tmp; } else if (act === 'down' && i < adoptedAddresses.length - 1) { const tmp = adoptedAddresses[i + 1]; adoptedAddresses[i + 1] = adoptedAddresses[i]; adoptedAddresses[i] = tmp; } renderAdoptedList(); renderSkillsList(); }); }); } function renderTools() { const q = String(inpToolSearch.value || '').trim().toLowerCase(); const filtered = DIDACTYL_TOOLS.filter((t) => { if (!q) return true; return t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q); }); divToolsList.innerHTML = filtered.map((t) => { const required = requiresTools.includes(t.name) ? 'required' : ''; return `
${escapeHtml(t.name)}
${escapeHtml(t.description)}
`; }).join(''); Array.from(divToolsList.querySelectorAll('.toolItem[data-tool]')).forEach((el) => { el.addEventListener('dblclick', () => { const tool = String(el.getAttribute('data-tool') || ''); if (!tool) return; if (!requiresTools.includes(tool)) requiresTools.push(tool); insertAtCursor(taSkillContent, `{{${tool}}}`); renderTagChips(); renderTools(); renderHighlight(); setStatus(`Added requires_tool and inserted {{${tool}}}`); }); }); } function insertAtCursor(textarea, text) { const start = textarea.selectionStart ?? textarea.value.length; const end = textarea.selectionEnd ?? textarea.value.length; const before = textarea.value.slice(0, start); const after = textarea.value.slice(end); textarea.value = `${before}${text}${after}`; const next = start + text.length; textarea.selectionStart = next; textarea.selectionEnd = next; textarea.focus(); } function renderHighlight() { let html = escapeHtml(taSkillContent.value || ''); html = html.replace(/\{\{\s*([a-zA-Z0-9_-]+)\s*\}\}/g, (m, name) => { if (TOOL_NAME_SET.has(name)) { return `{{${escapeHtml(name)}}}`; } return `{{${escapeHtml(name)}}}`; }); preSkillHighlight.innerHTML = html + '\n'; } function syncEditorScroll() { preSkillHighlight.scrollTop = taSkillContent.scrollTop; preSkillHighlight.scrollLeft = taSkillContent.scrollLeft; } function upsertSkill(skill) { if (!skill || !skill.address) return; const prev = skillsByAddress.get(skill.address); if (!prev || Number(skill.created_at || 0) >= Number(prev.created_at || 0)) { skillsByAddress.set(skill.address, skill); } } async function parseSkillEvent(evt) { if (!evt || (evt.kind !== 31123 && evt.kind !== 31124)) return null; const slug = normalizeSlug(getTagValue(evt.tags, 'd')); if (!slug) return null; let content = String(evt.content || ''); let tags = Array.isArray(evt.tags) ? evt.tags.map(t => Array.isArray(t) ? [...t] : t) : []; let decryptError = ''; if (evt.kind === 31124) { try { if (!window.nostr?.nip44?.decrypt) throw new Error('nip44 decrypt unavailable in signer'); const plaintext = await window.nostr.nip44.decrypt(String(evt.pubkey || ''), String(evt.content || '')); const parsed = JSON.parse(String(plaintext || '{}')); content = String(parsed?.content || ''); const privateTags = Array.isArray(parsed?.private_tags) ? parsed.private_tags : []; tags = [['d', slug], ...privateTags.filter(t => Array.isArray(t) && t.length >= 2)]; } catch (error) { decryptError = String(error?.message || error || 'decrypt failed'); } } const address = makeAddress(evt.kind, evt.pubkey, slug); return { address, key: `${address}`, kind: Number(evt.kind), slug, description: getTagValue(tags, 'description') || slug, trigger: getTagValue(tags, 'trigger'), filter: getTagValue(tags, 'filter'), llm: getTagValue(tags, 'llm'), temperature: getTagValue(tags, 'temperature'), max_tokens: getTagValue(tags, 'max_tokens'), seed: getTagValue(tags, 'seed'), requires_tool: getTagValues(tags, 'requires_tool'), optional_tool: getTagValues(tags, 'optional_tool'), requires_skill: getTagValues(tags, 'requires_skill'), content, rawTags: tags, rawEvent: evt, author: String(evt.pubkey || ''), created_at: Number(evt.created_at || 0), decryptError }; } function loadSkillIntoEditor(address) { const skill = skillsByAddress.get(address); selectedSkillAddress = address; renderSkillsList(); if (!skill) { setStatus('Skill record is not loaded yet.'); return; } selSkillKind.value = String(skill.kind || 31123); inpSkillSlug.value = skill.slug || ''; inpSkillDescription.value = skill.description || ''; selSkillTrigger.value = skill.trigger || ''; inpSkillFilter.value = skill.filter || ''; renderTriggerFilterHelper(); inpSkillLlm.value = skill.llm || ''; syncLlmHelperFromSkillInput(); inpSkillTemperature.value = skill.temperature || ''; inpSkillMaxTokens.value = skill.max_tokens || ''; inpSkillSeed.value = skill.seed || ''; taSkillContent.value = String(skill.content || ''); requiresTools = [...(skill.requires_tool || [])]; optionalTools = [...(skill.optional_tool || [])]; requiresSkills = [...(skill.requires_skill || [])]; renderTagChips(); renderTools(); renderHighlight(); syncEditorScroll(); const isForeignPublic = skill.kind === 31123 && skill.author && currentPubkey && skill.author !== currentPubkey; btnCopyFork.style.display = isForeignPublic ? '' : 'none'; if (skill.decryptError) { setStatus(`Loaded ${skill.slug} with decrypt warning: ${skill.decryptError}`); } else { setStatus(`Loaded ${skill.slug}`); } } async function handleIncomingSkillEvent(evt) { const parsed = await parseSkillEvent(evt); if (!parsed) return; upsertSkill(parsed); renderSkillsList(); renderAdoptedList(); renderTriggerFilterHelper(); if (selectedSkillAddress === parsed.address) { loadSkillIntoEditor(parsed.address); } } function parseAdoptionEvent(evt) { if (!evt || evt.kind !== 10123 || evt.pubkey !== currentPubkey) return; const tags = Array.isArray(evt.tags) ? evt.tags : []; adoptedAddresses = tags .filter(t => Array.isArray(t) && t[0] === 'a' && typeof t[1] === 'string') .map(t => String(t[1])); adoptionEventId = String(evt.id || ''); renderAdoptedList(); renderSkillsList(); } function buildSkillTags() { const tags = []; const slug = normalizeSlug(inpSkillSlug.value); const description = String(inpSkillDescription.value || '').trim(); const trigger = String(selSkillTrigger.value || '').trim(); const filter = String(inpSkillFilter.value || '').trim(); const llm = String(inpSkillLlm.value || '').trim(); const temperature = String(inpSkillTemperature.value || '').trim(); const maxTokens = String(inpSkillMaxTokens.value || '').trim(); const seed = String(inpSkillSeed.value || '').trim(); tags.push(['d', slug]); if (description) tags.push(['description', description]); if (trigger) tags.push(['trigger', trigger]); if (filter) tags.push(['filter', filter]); if (llm) tags.push(['llm', llm]); if (temperature) tags.push(['temperature', temperature]); if (maxTokens) tags.push(['max_tokens', maxTokens]); if (seed) tags.push(['seed', seed]); for (const t of requiresTools) tags.push(['requires_tool', t]); for (const t of optionalTools) tags.push(['optional_tool', t]); for (const s of requiresSkills) tags.push(['requires_skill', s]); return tags; } async function saveSkill(kind) { const slug = normalizeSlug(inpSkillSlug.value); if (!slug) return setStatus('Slug is required.'); const content = String(taSkillContent.value || '').trim(); if (!content) return setStatus('Skill content is required.'); const now = Math.floor(Date.now() / 1000); try { if (Number(kind) === 31123) { const event = { kind: 31123, content, tags: buildSkillTags(), created_at: now }; await publishEvent(event); setStatus(`Published public skill: ${slug}`); } else { if (!window.nostr?.nip44?.encrypt) { throw new Error('NIP-44 encryption unavailable in signer'); } const allTags = buildSkillTags(); const privateTags = allTags.filter((t) => Array.isArray(t) && t[0] !== 'd'); const payload = JSON.stringify({ content, private_tags: privateTags }); const ciphertext = await window.nostr.nip44.encrypt(currentPubkey, payload); const event = { kind: 31124, content: String(ciphertext || ''), tags: [['d', slug]], created_at: now }; await publishEvent(event); setStatus(`Published private skill: ${slug}`); } refreshSkills(); } catch (error) { setStatus(`Save failed: ${String(error?.message || error)}`); } } async function deleteSelectedSkill() { const skill = selectedSkillAddress ? skillsByAddress.get(selectedSkillAddress) : null; if (!skill) return setStatus('Select a skill first.'); if (skill.author !== currentPubkey) return setStatus('You can only delete your own skills.'); if (!confirm(`Delete skill "${skill.slug}"?`)) return; const event = { kind: 5, content: '', tags: [['a', skill.address]], created_at: Math.floor(Date.now() / 1000) }; if (skill.rawEvent?.id) event.tags.push(['e', skill.rawEvent.id]); try { await publishEvent(event); setStatus(`Deletion request published for ${skill.slug}`); } catch (error) { setStatus(`Delete failed: ${String(error?.message || error)}`); } } function currentEditorAddress() { const kind = Number(selSkillKind.value || 31123); const slug = normalizeSlug(inpSkillSlug.value); if (!slug || !currentPubkey) return null; return makeAddress(kind, currentPubkey, slug); } async function publishAdoptionList() { try { const event = { kind: 10123, content: '', tags: adoptedAddresses.map((addr) => ['a', addr]), created_at: Math.floor(Date.now() / 1000) }; if (adoptionEventId) event.tags.push(['e', adoptionEventId]); await publishEvent(event); setStatus(`Published adoption list (${adoptedAddresses.length} skills).`); } catch (error) { setStatus(`Publish 10123 failed: ${String(error?.message || error)}`); } } async function adoptCurrentSkill() { let addr = selectedSkillAddress; if (!addr) addr = currentEditorAddress(); if (!addr) return setStatus('Need selected skill or valid slug/kind to adopt.'); if (!adoptedAddresses.includes(addr)) adoptedAddresses.push(addr); renderAdoptedList(); renderSkillsList(); await publishAdoptionList(); } async function removeCurrentAdoptedSkill() { let addr = selectedSkillAddress; if (!addr) addr = currentEditorAddress(); if (!addr) return setStatus('Need selected skill or valid slug/kind.'); adoptedAddresses = adoptedAddresses.filter((x) => x !== addr); renderAdoptedList(); renderSkillsList(); await publishAdoptionList(); } function copySelectedSkillToMine() { const skill = selectedSkillAddress ? skillsByAddress.get(selectedSkillAddress) : null; if (!skill) return setStatus('Select a public skill to copy.'); if (skill.kind !== 31123 || skill.author === currentPubkey) return setStatus('Copy is for public skills by other authors.'); const nextSlug = normalizeSlug(`${skill.slug}-copy`) || `skill-copy-${Date.now()}`; selSkillKind.value = '31123'; inpSkillSlug.value = nextSlug; inpSkillDescription.value = `${skill.description || skill.slug} (copy)`; selSkillTrigger.value = skill.trigger || ''; inpSkillFilter.value = skill.filter || ''; inpSkillLlm.value = skill.llm || ''; syncLlmHelperFromSkillInput(); inpSkillTemperature.value = skill.temperature || ''; inpSkillMaxTokens.value = skill.max_tokens || ''; inpSkillSeed.value = skill.seed || ''; taSkillContent.value = skill.content || ''; requiresTools = [...(skill.requires_tool || [])]; optionalTools = [...(skill.optional_tool || [])]; requiresSkills = [...(skill.requires_skill || [])]; selectedSkillAddress = null; renderTagChips(); renderTools(); renderHighlight(); renderTriggerFilterHelper(); renderSkillsList(); btnCopyFork.style.display = 'none'; setStatus(`Copied ${skill.slug} to editor as ${nextSlug}. Save to publish.`); } function clearSub(refName) { try { const sub = (refName === 'my') ? mySkillSub : (refName === 'public') ? publicSkillSub : adoptionSub; if (sub && typeof sub.close === 'function') sub.close(); } catch (_error) {} if (refName === 'my') { mySkillSub = null; mySkillSubId = null; } else if (refName === 'public') { publicSkillSub = null; publicSkillSubId = null; } else { adoptionSub = null; adoptionSubId = null; } } function refreshMySkills() { clearSub('my'); mySkillSub = subscribe({ kinds: [31123, 31124], authors: [currentPubkey], limit: 400 }, { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }); mySkillSubId = mySkillSub?.subId || null; } function refreshPublicSkills() { clearSub('public'); publicSkillSub = subscribe({ kinds: [31123], limit: 500 }, { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }); publicSkillSubId = publicSkillSub?.subId || null; publicLoaded = true; } function refreshAdoption() { clearSub('adoption'); adoptionSub = subscribe({ kinds: [10123], authors: [currentPubkey], limit: 1 }, { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }); adoptionSubId = adoptionSub?.subId || null; } function refreshSkills() { setStatus('Refreshing skills...'); refreshMySkills(); refreshAdoption(); if (activeTab === 'public' || publicLoaded) refreshPublicSkills(); } function bindSkillEventListeners() { window.addEventListener('ndkEvent', async (event) => { const evt = event?.detail; if (!evt) return; if (evt.kind === 10123 && evt.pubkey === currentPubkey) { parseAdoptionEvent(evt); return; } if (evt.kind === 31123 || evt.kind === 31124) { await handleIncomingSkillEvent(evt); } }); window.addEventListener('ndkEose', (event) => { const sid = event?.detail?.subId || null; if (!sid) return; if (sid === mySkillSubId || sid === publicSkillSubId || sid === adoptionSubId) { if (!getVisibleSkills().length) { setStatus(activeTab === 'public' ? 'No public skills found yet.' : 'No skills found yet.'); } } }); } function bindUi() { btnTabMy.addEventListener('click', () => { activeTab = 'my'; renderSkillsList(); }); btnTabPublic.addEventListener('click', () => { activeTab = 'public'; if (!publicLoaded) refreshPublicSkills(); renderSkillsList(); }); btnTabAdopted.addEventListener('click', () => { activeTab = 'adopted'; renderSkillsList(); }); inpSkillSearch.addEventListener('input', renderSkillsList); inpToolSearch.addEventListener('input', renderTools); selSkillTrigger.addEventListener('change', renderTriggerFilterHelper); selLlmProvider?.addEventListener('change', () => selectLlmProvider(selLlmProvider.value)); selLlmModel?.addEventListener('change', renderLlmModelOptions); inpLlmModelFilter?.addEventListener('input', renderLlmModelOptions); btnLlmFavorite?.addEventListener('click', toggleCurrentModelFavorite); btnLlmApply?.addEventListener('click', applyLlmSelectionToInput); btnRefreshSkills.addEventListener('click', refreshSkills); btnNewSkill.addEventListener('click', clearEditor); taSkillContent.addEventListener('input', renderHighlight); taSkillContent.addEventListener('scroll', syncEditorScroll); window.addEventListener('resize', syncEditorScroll); btnAddRequiresSkill.addEventListener('click', () => { const v = normalizeSlug(inpRequiresSkill.value); if (!v) return; if (!requiresSkills.includes(v)) requiresSkills.push(v); inpRequiresSkill.value = ''; renderTagChips(); }); btnSavePublic.addEventListener('click', () => saveSkill(31123)); btnSavePrivate.addEventListener('click', () => saveSkill(31124)); btnDeleteSkill.addEventListener('click', deleteSelectedSkill); btnCopyFork.addEventListener('click', copySelectedSkillToMine); btnAdoptCurrent.addEventListener('click', adoptCurrentSkill); btnRemoveAdoptCurrent.addEventListener('click', removeCurrentAdoptedSkill); btnPublishAdoption.addEventListener('click', publishAdoptionList); btnCopyFork.style.display = 'none'; renderTriggerFilterHelper(); syncLlmHelperFromSkillInput(); } function initHamburgerMenu() { hamburgerInstance = new HamburgerMorphing('#divSvgHam', { foreground: 'var(--primary-color)', background: 'var(--secondary-color)', hover: 'var(--accent-color)' }); hamburgerInstance.animateTo('burger'); } function openNav() { divSideNav.style.zIndex = 3; divSideNav.style.width = 'clamp(400px, 50vw, 600px)'; isNavOpen = true; if (hamburgerInstance) hamburgerInstance.animateTo('arrow_left'); if (!logoutHamburger) { logoutHamburger = new HamburgerMorphing('#logoutHamburgerContainer', { size: 24, foreground: 'var(--primary-color)', background: 'var(--secondary-color)', hover: 'var(--accent-color)' }); logoutHamburger.animateTo('x'); } if (!themeToggleHamburger) { themeToggleHamburger = new HamburgerMorphing('#themeToggleHamburgerContainer', { size: 24, foreground: 'var(--primary-color)', background: 'var(--secondary-color)', hover: 'var(--accent-color)' }); const savedTheme = localStorage.getItem('theme'); isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode'); themeToggleHamburger.animateTo(isDarkMode ? 'moon' : 'circle'); } } function closeNav() { divSideNav.style.width = '0vw'; divSideNav.style.zIndex = -1; isNavOpen = false; if (hamburgerInstance) hamburgerInstance.animateTo('burger'); } function toggleNav() { if (isNavOpen) closeNav(); else openNav(); } function hasTargetPubkeyInUrl() { const params = new URLSearchParams(window.location.search || ''); return Boolean(String(params.get('npub') || '').trim() || String(params.get('pubkey') || '').trim()); } function resolveAuthModeFromUrl() { const params = new URLSearchParams(window.location.search || ''); const explicitAuth = String(params.get('auth') || '').trim().toLowerCase(); if (['required', 'optional', 'none'].includes(explicitAuth)) return explicitAuth; if (hasTargetPubkeyInUrl()) return 'optional'; return 'required'; } function isAuthRequiredError(error) { const message = String(error?.message || error || '').toLowerCase(); return message.includes('authentication required'); } async function initializeAuthentication(mode) { if (mode === 'required') { await initNDKPage(); currentPubkey = await getPubkey(); isAuthenticated = true; return; } if (mode === 'none') { isAuthenticated = false; currentPubkey = null; return; } try { await initNDKPage(); currentPubkey = await getPubkey(); isAuthenticated = true; } catch (error) { if (isAuthRequiredError(error)) { isAuthenticated = false; currentPubkey = null; return; } throw error; } } async function UpdateFooter() { try { await updateFooterRelayStatus(); await updateSidenavRelaySection(); await updateBlossomSection(); divFooterCenter.innerHTML = ''; divFooterRight.innerHTML = ''; } catch (error) { console.error('[skills-edit.html] footer update error:', error); } } async function initializeAuthenticatedPageFeatures() { if (!isAuthenticated || authedPageInitialized) return; await injectHeaderAvatar(currentPubkey); try { pageSettings = await getUserSettings(); } catch (_error) { pageSettings = {}; } if (!unsubscribeUserSettings) { unsubscribeUserSettings = onUserSettings((settings) => { pageSettings = settings || {}; initializeLlmHelperFromSettings(); }); } initFooterRelayStatus(); initSidenavRelaySection(); await initBlossomSection(); initAiSectionWithLocalConfig(); initializeLlmHelperFromSettings(); await UpdateFooter(); if (!updateIntervalId) updateIntervalId = setInterval(UpdateFooter, 1000); if (!relayActivityListenersBound) { window.addEventListener('ndkRelayActivity', (event) => { const { relayUrl, activity } = event.detail || {}; if (relayUrl && activity) setRelayActivityState(relayUrl, activity); }); window.addEventListener('message', (event) => { if (event.data && event.data.type === 'relayActivity') { const { relayUrl, activity } = event.data; if (relayUrl && activity) setRelayActivityState(relayUrl, activity); } }); relayActivityListenersBound = true; } bindUi(); bindSkillEventListeners(); renderTools(); clearEditor(); refreshSkills(); renderSkillsList(); renderAdoptedList(); authedPageInitialized = true; } async function promptLoginIfNeeded() { if (isAuthenticated) return true; await initNDKPage(); currentPubkey = await getPubkey(); isAuthenticated = true; await initializeAuthenticatedPageFeatures(); return true; } const Logout = async () => { if (updateIntervalId) { clearInterval(updateIntervalId); updateIntervalId = null; } clearSub('my'); clearSub('public'); clearSub('adoption'); disconnect(); if (window.NOSTR_LOGIN_LITE?.logout) await window.NOSTR_LOGIN_LITE.logout(); localStorage.clear(); sessionStorage.clear(); if (window.indexedDB) { const databases = await window.indexedDB.databases(); for (const db of databases) { if (db.name) window.indexedDB.deleteDatabase(db.name); } } location.reload(true); }; (async function main() { console.log('[skills-edit.html] Starting initialization...'); try { initHamburgerMenu(); const divSvgHam = document.getElementById('divSvgHam'); if (divSvgHam) divSvgHam.addEventListener('click', toggleNav); const themeToggleButton = document.getElementById('themeToggleButton'); const logoutButton = document.getElementById('logoutButton'); if (themeToggleButton) { themeToggleButton.addEventListener('click', () => { isDarkMode = !isDarkMode; localStorage.setItem('theme', isDarkMode ? 'dark' : 'light'); localStorage.setItem('sidenavWasOpen', isNavOpen ? 'true' : 'false'); window.location.reload(); }); } if (logoutButton) { logoutButton.addEventListener('click', async () => { try { if (!isAuthenticated) { await promptLoginIfNeeded(); return; } await Logout(); } catch (error) { console.error('[skills-edit.html] logout/login failed', error); } }); } authMode = resolveAuthModeFromUrl(); await initializeAuthentication(authMode); await initializeAuthenticatedPageFeatures(); const sidenavWasOpen = localStorage.getItem('sidenavWasOpen'); if (sidenavWasOpen === 'true') { localStorage.removeItem('sidenavWasOpen'); openNav(); } if (!isAuthenticated && (authMode === 'optional' || authMode === 'none')) { divFooterCenter.textContent = 'Public mode'; divFooterRight.textContent = 'Sign in from side menu for private features'; } await updateVersionDisplay(); } catch (error) { console.error('[skills-edit.html] Initialization failed:', error); divBody.innerHTML = `
❌ Initialization Error
${escapeHtml(error?.message || error)}
`; } })();