synabun 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,519 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Invite / Share
3
+ // ═══════════════════════════════════════════
4
+ // Toolbar dropdown for session invitation management.
5
+ // Follows the same pattern as ui-workspaces.js.
6
+
7
+ import { emit, on } from './state.js';
8
+ import {
9
+ fetchInviteStatus,
10
+ fetchTunnelStatus,
11
+ startTunnel,
12
+ stopTunnel,
13
+ generateInviteKey,
14
+ revokeInviteKey,
15
+ revokeAllInviteSessions,
16
+ saveInviteProxy,
17
+ fetchInvitePermissions,
18
+ saveInvitePermissions,
19
+ } from './api.js';
20
+ import { isGuest } from './ui-sync.js';
21
+
22
+ const $ = (id) => document.getElementById(id);
23
+
24
+ let _isOpen = false;
25
+ let _data = { hasKey: false, maskedKey: null, activeSessions: 0, proxyConfig: { useProxy: false, proxyUrl: '' } };
26
+ let _tunnelUrl = null;
27
+ let _tunnelAvailable = false;
28
+ let _tunnelStarting = false;
29
+ let _tunnelPollTimer = null;
30
+ let _keyMode = 'auto'; // 'auto' | 'custom'
31
+ let _revealedKey = null; // shown only right after generation
32
+ let _permissions = { terminal: false, whiteboard: false, memories: true, skills: false, cards: true, browser: false };
33
+
34
+ // ─── Dropdown open/close ────────────────
35
+
36
+ function openDropdown() {
37
+ const dd = $('invite-dropdown');
38
+ if (!dd) return;
39
+ _isOpen = true;
40
+ dd.style.display = '';
41
+ $('invite-btn')?.classList.add('active');
42
+ emit('panel:close-all-dropdowns-except', 'invite');
43
+ loadAndRender();
44
+ }
45
+
46
+ function closeDropdown() {
47
+ const dd = $('invite-dropdown');
48
+ if (!dd) return;
49
+ _isOpen = false;
50
+ dd.style.display = 'none';
51
+ $('invite-btn')?.classList.remove('active');
52
+ _revealedKey = null;
53
+ }
54
+
55
+ function toggleDropdown() {
56
+ _isOpen ? closeDropdown() : openDropdown();
57
+ }
58
+
59
+ // ─── Data fetching ──────────────────────
60
+
61
+ async function loadAndRender() {
62
+ try {
63
+ const [inviteRes, tunnelRes, permRes] = await Promise.all([
64
+ fetchInviteStatus(),
65
+ fetchTunnelStatus(),
66
+ fetchInvitePermissions(),
67
+ ]);
68
+ _data = inviteRes;
69
+ _tunnelUrl = tunnelRes.url || null;
70
+ _tunnelAvailable = tunnelRes.available;
71
+ _tunnelStarting = tunnelRes.starting || false;
72
+ if (permRes?.permissions) _permissions = permRes.permissions;
73
+ } catch (err) {
74
+ console.warn('[invite] Failed to load status:', err);
75
+ }
76
+ render();
77
+ }
78
+
79
+ // ─── Tunnel polling ─────────────────────
80
+
81
+ function pollForTunnelUrl() {
82
+ if (_tunnelPollTimer) clearInterval(_tunnelPollTimer);
83
+ let attempts = 0;
84
+ _tunnelPollTimer = setInterval(async () => {
85
+ attempts++;
86
+ try {
87
+ const res = await fetchTunnelStatus();
88
+ if (res.url) {
89
+ clearInterval(_tunnelPollTimer);
90
+ _tunnelPollTimer = null;
91
+ _tunnelUrl = res.url;
92
+ _tunnelStarting = false;
93
+ if (_isOpen) render();
94
+ } else if (attempts >= 30) {
95
+ clearInterval(_tunnelPollTimer);
96
+ _tunnelPollTimer = null;
97
+ _tunnelStarting = false;
98
+ if (_isOpen) render();
99
+ }
100
+ } catch {}
101
+ }, 1000);
102
+ }
103
+
104
+ // ─── URL helpers ────────────────────────
105
+
106
+ function getBaseUrl() {
107
+ if (_data.proxyConfig?.useProxy && _data.proxyConfig?.proxyUrl) {
108
+ return _data.proxyConfig.proxyUrl.replace(/\/+$/, '');
109
+ }
110
+ return _tunnelUrl || null;
111
+ }
112
+
113
+ function getInviteUrl() {
114
+ const base = getBaseUrl();
115
+ return base ? base + '/invite' : null;
116
+ }
117
+
118
+ // ─── Clipboard helper ───────────────────
119
+
120
+ function flashCopied(el) {
121
+ el.classList.add('show');
122
+ setTimeout(() => el.classList.remove('show'), 1500);
123
+ }
124
+
125
+ async function copyText(text, feedbackId) {
126
+ try {
127
+ await navigator.clipboard.writeText(text);
128
+ const el = $(feedbackId);
129
+ if (el) flashCopied(el);
130
+ } catch {}
131
+ }
132
+
133
+ // ─── Render ─────────────────────────────
134
+
135
+ function render() {
136
+ const dd = $('invite-dropdown');
137
+ if (!dd) return;
138
+
139
+ const baseUrl = getBaseUrl();
140
+ const inviteUrl = getInviteUrl();
141
+ const hasUrl = !!baseUrl;
142
+ const useProxy = _data.proxyConfig?.useProxy || false;
143
+
144
+ dd.innerHTML = `
145
+ <div class="invite-section">
146
+ <div class="invite-section-label">Connection</div>
147
+ ${useProxy ? `
148
+ <input type="text" class="invite-proxy-input" id="invite-proxy-url"
149
+ placeholder="https://your-domain.com"
150
+ value="${escHtml(_data.proxyConfig?.proxyUrl || '')}">
151
+ <div class="invite-action-row" style="margin-top:6px">
152
+ <button class="invite-btn primary" id="invite-proxy-save">Save</button>
153
+ <button class="invite-alt-link" id="invite-mode-tunnel">Use Cloudflare tunnel</button>
154
+ </div>
155
+ ` : `
156
+ ${hasUrl ? `
157
+ <div class="invite-status-row">
158
+ <span class="invite-status-dot active"></span>
159
+ <span class="invite-url-text">${baseUrl}</span>
160
+ <button class="invite-copy-icon" id="invite-copy-base-url" data-tooltip="Copy"><svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
161
+ <span class="invite-copied" id="invite-copied-url">Copied</span>
162
+ </div>
163
+ <div class="invite-action-row">
164
+ <button class="invite-btn" id="invite-tunnel-stop">Stop</button>
165
+ <button class="invite-alt-link" id="invite-mode-proxy">Use custom URL</button>
166
+ </div>
167
+ ` : `
168
+ ${_tunnelAvailable ? `
169
+ ${_tunnelStarting ? `
170
+ <div class="invite-action-row">
171
+ <button class="invite-btn" disabled style="opacity:0.5;cursor:default">
172
+ <svg viewBox="0 0 24 24" class="invite-spin"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
173
+ Starting...
174
+ </button>
175
+ </div>
176
+ ` : `
177
+ <div class="invite-action-row">
178
+ <button class="invite-btn primary" id="invite-tunnel-start">Start Tunnel</button>
179
+ <button class="invite-alt-link" id="invite-mode-proxy">Use custom URL</button>
180
+ </div>
181
+ `}
182
+ ` : `
183
+ <div class="invite-action-row">
184
+ <span style="font-size:10px;color:var(--t-dimmed)">cloudflared not available</span>
185
+ <button class="invite-alt-link" id="invite-mode-proxy">Use custom URL</button>
186
+ </div>
187
+ `}
188
+ `}
189
+ `}
190
+ </div>
191
+
192
+ <div class="invite-section">
193
+ <div class="invite-section-label">Invite Key</div>
194
+ ${_keyMode === 'custom' ? `
195
+ <input type="text" class="invite-custom-input" id="invite-custom-pw"
196
+ placeholder="Enter a password (min 6 chars)" autocomplete="off" spellcheck="false">
197
+ ` : ''}
198
+ ${_revealedKey ? `
199
+ <div class="invite-key-box">
200
+ ${escHtml(_revealedKey)}
201
+ <button class="invite-copy-icon" id="invite-copy-key" data-tooltip="Copy key"><svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
202
+ </div>
203
+ <div class="invite-key-meta">
204
+ <span class="invite-warn">Copy now — won't be shown again</span>
205
+ <span class="invite-copied" id="invite-copied-fb">Copied</span>
206
+ ${inviteUrl ? `<button class="invite-btn primary" id="invite-copy-url" style="margin-left:auto">Copy Link</button>` : ''}
207
+ </div>
208
+ ` : `
209
+ <div class="invite-action-row">
210
+ ${_data.hasKey ? `
211
+ <span class="invite-key-status">${_data.maskedKey}</span>
212
+ <button class="invite-btn primary" id="invite-generate">Rotate</button>
213
+ <button class="invite-btn danger" id="invite-revoke-key">Revoke</button>
214
+ ` : `
215
+ <button class="invite-btn primary" id="invite-generate">Generate</button>
216
+ `}
217
+ <button class="invite-alt-link" id="${_keyMode === 'custom' ? 'invite-km-auto' : 'invite-km-custom'}">${_keyMode === 'custom' ? 'Use auto key' : 'Custom password'}</button>
218
+ </div>
219
+ `}
220
+ </div>
221
+
222
+ ${_data.activeSessions > 0 ? `
223
+ <div class="invite-section">
224
+ <div class="invite-sessions-row">
225
+ <span class="invite-sessions-dot"></span>
226
+ <span>${_data.activeSessions} active session${_data.activeSessions !== 1 ? 's' : ''}</span>
227
+ <button class="invite-btn danger" id="invite-revoke-sessions" style="margin-left:auto">Revoke</button>
228
+ </div>
229
+ </div>
230
+ ` : ''}
231
+
232
+ ${_data.hasKey ? `
233
+ <div class="invite-section">
234
+ <div class="invite-section-label">Guest access</div>
235
+ ${renderPermToggle('memories', 'Memories')}
236
+ ${renderPermToggle('cards', 'Memory Cards')}
237
+ ${renderPermToggle('whiteboard', 'Whiteboard')}
238
+ ${renderPermToggle('terminal', 'Terminal')}
239
+ ${renderPermToggle('skills', 'Skills Studio')}
240
+ ${renderPermToggle('browser', 'Browser')}
241
+ </div>
242
+ ` : ''}
243
+ `;
244
+
245
+ wireEvents();
246
+ updateBadge();
247
+ }
248
+
249
+ function escHtml(s) {
250
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
251
+ }
252
+
253
+ const PERM_ICONS = {
254
+ memories: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 8v8"/><path d="M8 12h8"/></svg>',
255
+ cards: '<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>',
256
+ whiteboard: '<svg viewBox="0 0 24 24"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>',
257
+ terminal: '<svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>',
258
+ skills: '<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
259
+ browser: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
260
+ };
261
+
262
+ function renderPermToggle(key, label) {
263
+ const on = !!_permissions[key];
264
+ return `<div class="invite-perm-row" data-perm="${key}">
265
+ <div class="invite-perm-left">
266
+ <span class="invite-perm-icon">${PERM_ICONS[key] || ''}</span>
267
+ <span class="invite-perm-label">${label}</span>
268
+ </div>
269
+ <div class="invite-perm-toggle ${on ? 'on' : ''}"><div class="invite-perm-knob"></div></div>
270
+ </div>`;
271
+ }
272
+
273
+ // ─── Event wiring ───────────────────────
274
+
275
+ function wireEvents() {
276
+ // URL mode pills
277
+ $('invite-mode-tunnel')?.addEventListener('click', async (e) => {
278
+ e.stopPropagation();
279
+ await saveInviteProxy({ useProxy: false, proxyUrl: _data.proxyConfig?.proxyUrl || '' });
280
+ _data.proxyConfig = { useProxy: false, proxyUrl: _data.proxyConfig?.proxyUrl || '' };
281
+ _revealedKey = null;
282
+ render();
283
+ });
284
+
285
+ $('invite-mode-proxy')?.addEventListener('click', async (e) => {
286
+ e.stopPropagation();
287
+ await saveInviteProxy({ useProxy: true, proxyUrl: _data.proxyConfig?.proxyUrl || '' });
288
+ _data.proxyConfig = { useProxy: true, proxyUrl: _data.proxyConfig?.proxyUrl || '' };
289
+ _revealedKey = null;
290
+ render();
291
+ });
292
+
293
+ // Copy base URL
294
+ $('invite-copy-base-url')?.addEventListener('click', (e) => {
295
+ e.stopPropagation();
296
+ const url = getBaseUrl();
297
+ if (url) copyText(url, 'invite-copied-url');
298
+ });
299
+
300
+ // Tunnel start/stop
301
+ $('invite-tunnel-start')?.addEventListener('click', async (e) => {
302
+ e.stopPropagation();
303
+ try {
304
+ _tunnelStarting = true;
305
+ render();
306
+ await startTunnel();
307
+ pollForTunnelUrl();
308
+ } catch (err) {
309
+ console.error('[invite] Tunnel start failed:', err);
310
+ _tunnelStarting = false;
311
+ render();
312
+ }
313
+ });
314
+
315
+ $('invite-tunnel-stop')?.addEventListener('click', async (e) => {
316
+ e.stopPropagation();
317
+ try {
318
+ await stopTunnel();
319
+ _tunnelUrl = null;
320
+ _tunnelStarting = false;
321
+ if (_tunnelPollTimer) { clearInterval(_tunnelPollTimer); _tunnelPollTimer = null; }
322
+ render();
323
+ } catch (err) {
324
+ console.error('[invite] Tunnel stop failed:', err);
325
+ }
326
+ });
327
+
328
+ // Proxy save
329
+ $('invite-proxy-save')?.addEventListener('click', async (e) => {
330
+ e.stopPropagation();
331
+ const url = $('invite-proxy-url')?.value.trim() || '';
332
+ await saveInviteProxy({ useProxy: true, proxyUrl: url });
333
+ _data.proxyConfig = { useProxy: true, proxyUrl: url };
334
+ _revealedKey = null;
335
+ render();
336
+ });
337
+
338
+ // Key mode pills
339
+ $('invite-km-auto')?.addEventListener('click', (e) => {
340
+ e.stopPropagation();
341
+ _keyMode = 'auto';
342
+ _revealedKey = null;
343
+ render();
344
+ });
345
+
346
+ $('invite-km-custom')?.addEventListener('click', (e) => {
347
+ e.stopPropagation();
348
+ _keyMode = 'custom';
349
+ _revealedKey = null;
350
+ render();
351
+ });
352
+
353
+ // Generate / rotate
354
+ $('invite-generate')?.addEventListener('click', async (e) => {
355
+ e.stopPropagation();
356
+ try {
357
+ let res;
358
+ if (_keyMode === 'custom') {
359
+ const pw = $('invite-custom-pw')?.value.trim();
360
+ if (!pw || pw.length < 6) {
361
+ $('invite-custom-pw')?.focus();
362
+ return;
363
+ }
364
+ res = await generateInviteKey(pw);
365
+ } else {
366
+ res = await generateInviteKey();
367
+ }
368
+ _revealedKey = res.key;
369
+ _data.hasKey = true;
370
+ _data.maskedKey = '***' + res.key.slice(-8);
371
+ _data.activeSessions = 0;
372
+ render();
373
+ } catch (err) {
374
+ console.error('[invite] Generate failed:', err);
375
+ }
376
+ });
377
+
378
+ // Copy key
379
+ $('invite-copy-key')?.addEventListener('click', (e) => {
380
+ e.stopPropagation();
381
+ if (_revealedKey) copyText(_revealedKey, 'invite-copied-fb');
382
+ });
383
+
384
+ // Copy URL (with key as fragment)
385
+ $('invite-copy-url')?.addEventListener('click', (e) => {
386
+ e.stopPropagation();
387
+ const url = getInviteUrl();
388
+ if (url && _revealedKey) {
389
+ copyText(url + '#' + encodeURIComponent(_revealedKey), 'invite-copied-fb');
390
+ }
391
+ });
392
+
393
+ // Revoke key
394
+ $('invite-revoke-key')?.addEventListener('click', async (e) => {
395
+ e.stopPropagation();
396
+ try {
397
+ await revokeInviteKey();
398
+ _data.hasKey = false;
399
+ _data.maskedKey = null;
400
+ _data.activeSessions = 0;
401
+ _revealedKey = null;
402
+ render();
403
+ } catch (err) {
404
+ console.error('[invite] Revoke key failed:', err);
405
+ }
406
+ });
407
+
408
+ // Revoke sessions
409
+ $('invite-revoke-sessions')?.addEventListener('click', async (e) => {
410
+ e.stopPropagation();
411
+ try {
412
+ await revokeAllInviteSessions();
413
+ _data.activeSessions = 0;
414
+ render();
415
+ } catch (err) {
416
+ console.error('[invite] Revoke sessions failed:', err);
417
+ }
418
+ });
419
+
420
+ // Stop clicks inside dropdown from closing it
421
+ $('invite-dropdown')?.addEventListener('click', (e) => e.stopPropagation());
422
+
423
+ // Stop input/button focus from closing
424
+ const proxyInput = $('invite-proxy-url');
425
+ if (proxyInput) {
426
+ proxyInput.addEventListener('click', (e) => e.stopPropagation());
427
+ proxyInput.addEventListener('keydown', (e) => {
428
+ if (e.key === 'Enter') {
429
+ e.preventDefault();
430
+ $('invite-proxy-save')?.click();
431
+ }
432
+ });
433
+ }
434
+
435
+ const customInput = $('invite-custom-pw');
436
+ if (customInput) {
437
+ customInput.addEventListener('click', (e) => e.stopPropagation());
438
+ customInput.addEventListener('keydown', (e) => {
439
+ if (e.key === 'Enter') {
440
+ e.preventDefault();
441
+ $('invite-generate')?.click();
442
+ }
443
+ });
444
+ // Auto-focus the custom password input
445
+ setTimeout(() => customInput.focus(), 50);
446
+ }
447
+
448
+ // Permission toggles
449
+ $('invite-dropdown')?.querySelectorAll('.invite-perm-row').forEach((row) => {
450
+ row.addEventListener('click', async (e) => {
451
+ e.stopPropagation();
452
+ const key = row.dataset.perm;
453
+ if (!key) return;
454
+ const newVal = !_permissions[key];
455
+ _permissions[key] = newVal;
456
+ // Optimistic toggle
457
+ const toggle = row.querySelector('.invite-perm-toggle');
458
+ if (toggle) toggle.classList.toggle('on', newVal);
459
+ try {
460
+ await saveInvitePermissions({ [key]: newVal });
461
+ } catch (err) {
462
+ // Revert on failure
463
+ _permissions[key] = !newVal;
464
+ if (toggle) toggle.classList.toggle('on', !newVal);
465
+ console.error('[invite] Permission save failed:', err);
466
+ }
467
+ });
468
+ });
469
+ }
470
+
471
+ // ─── Badge ──────────────────────────────
472
+
473
+ function updateBadge() {
474
+ const badge = $('invite-session-count');
475
+ if (!badge) return;
476
+ if (_data.activeSessions > 0) {
477
+ badge.textContent = _data.activeSessions;
478
+ badge.style.display = '';
479
+ } else {
480
+ badge.style.display = 'none';
481
+ }
482
+ }
483
+
484
+ // ─── Init ───────────────────────────────
485
+
486
+ export function initInvite() {
487
+ const btn = $('invite-btn');
488
+ if (!btn) return;
489
+
490
+ btn.addEventListener('click', (e) => {
491
+ e.stopPropagation();
492
+ toggleDropdown();
493
+ });
494
+
495
+ // Close on outside click
496
+ document.addEventListener('click', (e) => {
497
+ if (_isOpen && !e.target.closest('#invite-overlay')) {
498
+ closeDropdown();
499
+ }
500
+ });
501
+
502
+ // Close when other dropdowns request exclusivity
503
+ on('panel:close-all-dropdowns', () => closeDropdown());
504
+ on('panel:close-all-dropdowns-except', (name) => {
505
+ if (name !== 'invite') closeDropdown();
506
+ });
507
+
508
+ // Hide Share button for guests (admin only)
509
+ on('session:info', ({ isGuest: guest }) => {
510
+ const overlay = $('invite-overlay');
511
+ if (overlay) overlay.style.display = guest ? 'none' : '';
512
+ });
513
+
514
+ // Load initial badge count
515
+ fetchInviteStatus().then((res) => {
516
+ _data = res;
517
+ updateBadge();
518
+ }).catch(() => {});
519
+ }