synabun 2026.3.316 → 2026.3.3102

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.
@@ -1,299 +1,335 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>SynaBun - Offline</title>
7
- <style>
8
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
-
10
- body {
11
- background: #030305;
12
- color: rgba(255,255,255,0.7);
13
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
14
- min-height: 100vh;
15
- display: flex;
16
- align-items: center;
17
- justify-content: center;
18
- overflow: hidden;
19
- }
20
-
21
- .card {
22
- background: transparent;
23
- border: none;
24
- padding: 48px 40px 40px;
25
- text-align: center;
26
- max-width: 420px;
27
- width: 90%;
28
- position: relative;
29
- }
30
-
31
- /* --- Mascot --- */
32
- .mascot-wrap {
33
- display: inline-block;
34
- position: relative;
35
- animation: breathe 4s ease-in-out infinite;
36
- margin-bottom: 28px;
37
- }
38
-
39
- @keyframes breathe {
40
- 0%, 100% { transform: translateY(0); }
41
- 50% { transform: translateY(-5px); }
42
- }
43
-
44
- /* --- Floating Z's --- */
45
- .zzz {
46
- position: absolute;
47
- top: -4px;
48
- right: -18px;
49
- font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
50
- font-weight: 700;
51
- color: rgba(110,200,255,0.45);
52
- pointer-events: none;
53
- }
54
-
55
- .zzz span {
56
- position: absolute;
57
- bottom: 0;
58
- right: 0;
59
- opacity: 0;
60
- animation: floatZ 3s ease-out infinite;
61
- }
62
-
63
- .zzz span:nth-child(1) { font-size: 14px; animation-delay: 0s; }
64
- .zzz span:nth-child(2) { font-size: 18px; animation-delay: 1s; }
65
- .zzz span:nth-child(3) { font-size: 22px; animation-delay: 2s; }
66
-
67
- @keyframes floatZ {
68
- 0% { opacity: 0; transform: translate(0, 0) scale(0.7); }
69
- 12% { opacity: 0.7; }
70
- 100% { opacity: 0; transform: translate(28px, -55px) scale(1.3); }
71
- }
72
-
73
- /* --- Text --- */
74
- .heading {
75
- font-size: 20px;
76
- font-weight: 600;
77
- color: rgba(255,255,255,0.8);
78
- margin-bottom: 6px;
79
- letter-spacing: -0.01em;
80
- }
81
-
82
- .subtext {
83
- font-size: 13px;
84
- color: rgba(255,255,255,0.35);
85
- margin-bottom: 28px;
86
- }
87
-
88
- /* --- Command block --- */
89
- .cmd-block {
90
- background: rgba(255,255,255,0.03);
91
- border: 1px solid rgba(255,255,255,0.06);
92
- border-radius: 10px;
93
- padding: 14px 16px;
94
- display: flex;
95
- align-items: center;
96
- justify-content: center;
97
- gap: 12px;
98
- margin-bottom: 24px;
99
- }
100
-
101
- .cmd-text {
102
- flex: 1;
103
- text-align: left;
104
- font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
105
- font-size: 11.5px;
106
- color: rgba(255,255,255,0.55);
107
- line-height: 1.5;
108
- word-break: break-all;
109
- user-select: all;
110
- }
111
-
112
- .cmd-text span {
113
- color: rgba(110,200,255,0.6);
114
- }
115
-
116
- .copy-btn {
117
- flex-shrink: 0;
118
- background: transparent;
119
- border: 1px solid rgba(255,255,255,0.08);
120
- border-radius: 6px;
121
- color: rgba(255,255,255,0.35);
122
- font-family: inherit;
123
- font-size: 11px;
124
- font-weight: 500;
125
- padding: 6px 12px;
126
- cursor: pointer;
127
- transition: background 0.15s, color 0.15s, border-color 0.15s;
128
- white-space: nowrap;
129
- }
130
-
131
- .copy-btn:hover {
132
- background: rgba(255,255,255,0.06);
133
- color: rgba(255,255,255,0.6);
134
- border-color: rgba(255,255,255,0.12);
135
- }
136
-
137
- .copy-btn.copied {
138
- background: rgba(110,200,255,0.12);
139
- color: rgba(110,200,255,0.8);
140
- border-color: rgba(110,200,255,0.2);
141
- }
142
-
143
- /* --- Retry indicator --- */
144
- .retry {
145
- display: flex;
146
- align-items: center;
147
- justify-content: center;
148
- gap: 8px;
149
- font-size: 11px;
150
- color: rgba(255,255,255,0.25);
151
- }
152
-
153
- .retry-dot {
154
- width: 6px;
155
- height: 6px;
156
- border-radius: 50%;
157
- background: rgba(110,200,255,0.4);
158
- animation: pulse 2s ease-in-out infinite;
159
- }
160
-
161
- @keyframes pulse {
162
- 0%, 100% { opacity: 0.4; transform: scale(1); }
163
- 50% { opacity: 1; transform: scale(1.3); }
164
- }
165
-
166
- .retry.connected .retry-dot {
167
- background: rgba(80,220,120,0.7);
168
- animation: none;
169
- }
170
-
171
- .retry.connected {
172
- color: rgba(80,220,120,0.6);
173
- }
174
-
175
- /* --- Ambient particles --- */
176
- .particles {
177
- position: fixed;
178
- inset: 0;
179
- pointer-events: none;
180
- z-index: -1;
181
- overflow: hidden;
182
- }
183
-
184
- .particles span {
185
- position: absolute;
186
- width: 2px;
187
- height: 2px;
188
- border-radius: 50%;
189
- background: rgba(130,145,195,0.15);
190
- animation: drift linear infinite;
191
- }
192
-
193
- @keyframes drift {
194
- 0% { transform: translateY(100vh) translateX(0); opacity: 0; }
195
- 10% { opacity: 1; }
196
- 90% { opacity: 1; }
197
- 100% { transform: translateY(-10vh) translateX(30px); opacity: 0; }
198
- }
199
- </style>
200
- </head>
201
- <body>
202
-
203
- <!-- Ambient particles -->
204
- <div class="particles" id="particles"></div>
205
-
206
- <div class="card">
207
- <!-- Sleeping mascot -->
208
- <div class="mascot-wrap">
209
- <svg viewBox="0 0 280 140" width="180" height="90" style="overflow:visible">
210
- <!-- Left eye — closed -->
211
- <rect x="80" y="57" width="38" height="6" rx="3" fill="rgba(255,255,255,0.45)"/>
212
- <!-- Right eye — closed -->
213
- <rect x="162" y="57" width="38" height="6" rx="3" fill="rgba(255,255,255,0.45)"/>
214
- <!-- Peaceful smile -->
215
- <path d="M118,90 Q140,100 162,90" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="2.5" stroke-linecap="round"/>
216
- </svg>
217
- <div class="zzz">
218
- <span>z</span>
219
- <span>z</span>
220
- <span>z</span>
221
- </div>
222
- </div>
223
-
224
- <div class="heading">Server is sleeping</div>
225
- <div class="subtext">The Neural Interface server isn't running.</div>
226
-
227
- <div class="cmd-block">
228
- <div class="cmd-text" id="cmdText">
229
- <span>npm</span> start
230
- </div>
231
- <button class="copy-btn" id="copyBtn">Copy</button>
232
- </div>
233
-
234
- <div class="retry" id="retry">
235
- <div class="retry-dot"></div>
236
- <span id="retryText">Checking for server...</span>
237
- </div>
238
- </div>
239
-
240
- <script>
241
- // --- Build restart command ---
242
- // Use the global `synabun` command (works from any directory)
243
- // Fallback: cd + npm start with `;` separator (works in both PowerShell and bash)
244
- const projectDir = localStorage.getItem('synabun-project-dir');
245
- const cmd = 'synabun';
246
- const cmdText = document.getElementById('cmdText');
247
- cmdText.innerHTML = `<span>synabun</span>`;
248
- const copyBtn = document.getElementById('copyBtn');
249
-
250
- copyBtn.addEventListener('click', async () => {
251
- try {
252
- await navigator.clipboard.writeText(cmd);
253
- } catch {
254
- const ta = document.createElement('textarea');
255
- ta.value = cmd;
256
- ta.style.position = 'fixed';
257
- ta.style.opacity = '0';
258
- document.body.appendChild(ta);
259
- ta.select();
260
- document.execCommand('copy');
261
- document.body.removeChild(ta);
262
- }
263
- copyBtn.textContent = 'Copied!';
264
- copyBtn.classList.add('copied');
265
- setTimeout(() => {
266
- copyBtn.textContent = 'Copy';
267
- copyBtn.classList.remove('copied');
268
- }, 1500);
269
- });
270
-
271
- // --- Auto-retry ---
272
- const retry = document.getElementById('retry');
273
- const retryText = document.getElementById('retryText');
274
-
275
- const interval = setInterval(async () => {
276
- try {
277
- const res = await fetch('/', { method: 'HEAD', cache: 'no-store' });
278
- if (res.ok) {
279
- clearInterval(interval);
280
- retry.classList.add('connected');
281
- retryText.textContent = 'Server is back! Reconnecting...';
282
- setTimeout(() => location.reload(), 600);
283
- }
284
- } catch {}
285
- }, 3000);
286
-
287
- // --- Ambient particles ---
288
- const container = document.getElementById('particles');
289
- for (let i = 0; i < 20; i++) {
290
- const p = document.createElement('span');
291
- p.style.left = Math.random() * 100 + '%';
292
- p.style.animationDuration = (12 + Math.random() * 18) + 's';
293
- p.style.animationDelay = -(Math.random() * 20) + 's';
294
- p.style.width = p.style.height = (1.5 + Math.random() * 1.5) + 'px';
295
- container.appendChild(p);
296
- }
297
- </script>
298
- </body>
299
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SynaBun - Offline</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ body {
11
+ background: #030305;
12
+ color: rgba(255,255,255,0.7);
13
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
14
+ min-height: 100vh;
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .card {
22
+ background: transparent;
23
+ border: none;
24
+ padding: 48px 40px 40px;
25
+ text-align: center;
26
+ max-width: 420px;
27
+ width: 90%;
28
+ position: relative;
29
+ }
30
+
31
+ /* --- Mascot --- */
32
+ .mascot-wrap {
33
+ display: inline-block;
34
+ position: relative;
35
+ animation: breathe 4s ease-in-out infinite;
36
+ margin-bottom: 28px;
37
+ }
38
+
39
+ @keyframes breathe {
40
+ 0%, 100% { transform: translateY(0); }
41
+ 50% { transform: translateY(-5px); }
42
+ }
43
+
44
+ /* --- Floating Z's --- */
45
+ .zzz {
46
+ position: absolute;
47
+ top: -4px;
48
+ right: -18px;
49
+ font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
50
+ font-weight: 700;
51
+ color: rgba(110,200,255,0.45);
52
+ pointer-events: none;
53
+ }
54
+
55
+ .zzz span {
56
+ position: absolute;
57
+ bottom: 0;
58
+ right: 0;
59
+ opacity: 0;
60
+ animation: floatZ 3s ease-out infinite;
61
+ }
62
+
63
+ .zzz span:nth-child(1) { font-size: 14px; animation-delay: 0s; }
64
+ .zzz span:nth-child(2) { font-size: 18px; animation-delay: 1s; }
65
+ .zzz span:nth-child(3) { font-size: 22px; animation-delay: 2s; }
66
+
67
+ @keyframes floatZ {
68
+ 0% { opacity: 0; transform: translate(0, 0) scale(0.7); }
69
+ 12% { opacity: 0.7; }
70
+ 100% { opacity: 0; transform: translate(28px, -55px) scale(1.3); }
71
+ }
72
+
73
+ /* --- Text --- */
74
+ .heading {
75
+ font-size: 20px;
76
+ font-weight: 600;
77
+ color: rgba(255,255,255,0.8);
78
+ margin-bottom: 6px;
79
+ letter-spacing: -0.01em;
80
+ }
81
+
82
+ .subtext {
83
+ font-size: 13px;
84
+ color: rgba(255,255,255,0.35);
85
+ margin-bottom: 28px;
86
+ }
87
+
88
+ /* --- Command labels --- */
89
+ .cmd-label {
90
+ font-size: 10px;
91
+ color: rgba(255,255,255,0.2);
92
+ margin-bottom: 6px;
93
+ text-transform: uppercase;
94
+ letter-spacing: 0.05em;
95
+ }
96
+
97
+ /* --- Command block --- */
98
+
99
+ .cmd-block {
100
+ background: rgba(255,255,255,0.03);
101
+ border: 1px solid rgba(255,255,255,0.06);
102
+ border-radius: 10px;
103
+ padding: 14px 16px;
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ gap: 12px;
108
+ margin-bottom: 8px;
109
+ }
110
+
111
+ .cmd-text {
112
+ flex: 1;
113
+ text-align: left;
114
+ font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
115
+ font-size: 11.5px;
116
+ color: rgba(255,255,255,0.55);
117
+ line-height: 1.5;
118
+ word-break: break-all;
119
+ user-select: all;
120
+ }
121
+
122
+ .cmd-text span {
123
+ color: rgba(110,200,255,0.6);
124
+ }
125
+
126
+ .cmd-or {
127
+ font-size: 10px;
128
+ color: rgba(255,255,255,0.15);
129
+ text-align: center;
130
+ margin-bottom: 8px;
131
+ }
132
+
133
+ .copy-btn {
134
+ flex-shrink: 0;
135
+ background: transparent;
136
+ border: 1px solid rgba(255,255,255,0.08);
137
+ border-radius: 6px;
138
+ color: rgba(255,255,255,0.35);
139
+ font-family: inherit;
140
+ font-size: 11px;
141
+ font-weight: 500;
142
+ padding: 6px 12px;
143
+ cursor: pointer;
144
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
145
+ white-space: nowrap;
146
+ }
147
+
148
+ .copy-btn:hover {
149
+ background: rgba(255,255,255,0.06);
150
+ color: rgba(255,255,255,0.6);
151
+ border-color: rgba(255,255,255,0.12);
152
+ }
153
+
154
+ .copy-btn.copied {
155
+ background: rgba(110,200,255,0.12);
156
+ color: rgba(110,200,255,0.8);
157
+ border-color: rgba(110,200,255,0.2);
158
+ }
159
+
160
+ /* --- Retry indicator --- */
161
+ .retry {
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ gap: 8px;
166
+ font-size: 11px;
167
+ color: rgba(255,255,255,0.25);
168
+ }
169
+
170
+ .retry-dot {
171
+ width: 6px;
172
+ height: 6px;
173
+ border-radius: 50%;
174
+ background: rgba(110,200,255,0.4);
175
+ animation: pulse 2s ease-in-out infinite;
176
+ }
177
+
178
+ @keyframes pulse {
179
+ 0%, 100% { opacity: 0.4; transform: scale(1); }
180
+ 50% { opacity: 1; transform: scale(1.3); }
181
+ }
182
+
183
+ .retry.connected .retry-dot {
184
+ background: rgba(80,220,120,0.7);
185
+ animation: none;
186
+ }
187
+
188
+ .retry.connected {
189
+ color: rgba(80,220,120,0.6);
190
+ }
191
+
192
+ /* --- Ambient particles --- */
193
+ .particles {
194
+ position: fixed;
195
+ inset: 0;
196
+ pointer-events: none;
197
+ z-index: -1;
198
+ overflow: hidden;
199
+ }
200
+
201
+ .particles span {
202
+ position: absolute;
203
+ width: 2px;
204
+ height: 2px;
205
+ border-radius: 50%;
206
+ background: rgba(130,145,195,0.15);
207
+ animation: drift linear infinite;
208
+ }
209
+
210
+ @keyframes drift {
211
+ 0% { transform: translateY(100vh) translateX(0); opacity: 0; }
212
+ 10% { opacity: 1; }
213
+ 90% { opacity: 1; }
214
+ 100% { transform: translateY(-10vh) translateX(30px); opacity: 0; }
215
+ }
216
+ </style>
217
+ </head>
218
+ <body>
219
+
220
+ <!-- Ambient particles -->
221
+ <div class="particles" id="particles"></div>
222
+
223
+ <div class="card">
224
+ <!-- Sleeping mascot -->
225
+ <div class="mascot-wrap">
226
+ <svg viewBox="0 0 280 140" width="180" height="90" style="overflow:visible">
227
+ <!-- Left eye — closed -->
228
+ <rect x="80" y="57" width="38" height="6" rx="3" fill="rgba(255,255,255,0.45)"/>
229
+ <!-- Right eye — closed -->
230
+ <rect x="162" y="57" width="38" height="6" rx="3" fill="rgba(255,255,255,0.45)"/>
231
+ <!-- Peaceful smile -->
232
+ <path d="M118,90 Q140,100 162,90" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="2.5" stroke-linecap="round"/>
233
+ </svg>
234
+ <div class="zzz">
235
+ <span>z</span>
236
+ <span>z</span>
237
+ <span>z</span>
238
+ </div>
239
+ </div>
240
+
241
+ <div class="heading">Server is sleeping</div>
242
+ <div class="subtext">The Neural Interface server isn't running.</div>
243
+
244
+ <div>
245
+ <div class="cmd-label">npm install</div>
246
+ <div class="cmd-block">
247
+ <div class="cmd-text" id="cmdText"><span>synabun</span></div>
248
+ <button class="copy-btn" id="copyBtn">Copy</button>
249
+ </div>
250
+ <div class="cmd-or" id="cmdOr" style="display:none">or</div>
251
+ <div class="cmd-label" id="cmdFallbackLabel" style="display:none">git / local install</div>
252
+ <div class="cmd-block" id="cmdFallback" style="display:none">
253
+ <div class="cmd-text" id="cmdFallbackText"></div>
254
+ <button class="copy-btn" id="copyFallbackBtn">Copy</button>
255
+ </div>
256
+ </div>
257
+
258
+ <div class="retry" id="retry">
259
+ <div class="retry-dot"></div>
260
+ <span id="retryText">Checking for server...</span>
261
+ </div>
262
+ </div>
263
+
264
+ <script>
265
+ // --- Platform detection ---
266
+ const ua = navigator.userAgent;
267
+ const isWin = /Win/.test(navigator.platform) || /Windows/.test(ua);
268
+ const isMac = /Mac/.test(navigator.platform);
269
+ // Linux = everything else
270
+
271
+ const projectDir = localStorage.getItem('synabun-project-dir');
272
+
273
+ // --- Manual command fallback ---
274
+ if (projectDir) {
275
+ const sep = isWin ? ' & ' : ' ; ';
276
+ const fallbackCmd = `cd "${projectDir}"${sep}npm start`;
277
+ const cmdOr = document.getElementById('cmdOr');
278
+ const cmdFallback = document.getElementById('cmdFallback');
279
+ const cmdFallbackText = document.getElementById('cmdFallbackText');
280
+ cmdOr.style.display = '';
281
+ cmdFallback.style.display = '';
282
+ const cmdFallbackLabel = document.getElementById('cmdFallbackLabel');
283
+ if (cmdFallbackLabel) cmdFallbackLabel.style.display = '';
284
+ cmdFallbackText.innerHTML = `<span>cd</span> "${projectDir}"${sep}<span>npm</span> start`;
285
+ document.getElementById('copyFallbackBtn').addEventListener('click', function() { copyToClipboard(fallbackCmd, this); });
286
+ }
287
+
288
+ // --- Copy helper ---
289
+ function copyToClipboard(text, btn) {
290
+ navigator.clipboard.writeText(text).catch(() => {
291
+ const ta = document.createElement('textarea');
292
+ ta.value = text;
293
+ ta.style.position = 'fixed';
294
+ ta.style.opacity = '0';
295
+ document.body.appendChild(ta);
296
+ ta.select();
297
+ document.execCommand('copy');
298
+ document.body.removeChild(ta);
299
+ });
300
+ btn.textContent = 'Copied!';
301
+ btn.classList.add('copied');
302
+ setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1500);
303
+ }
304
+
305
+ document.getElementById('copyBtn').addEventListener('click', function() { copyToClipboard('synabun', this); });
306
+
307
+ // --- Auto-retry ---
308
+ const retry = document.getElementById('retry');
309
+ const retryText = document.getElementById('retryText');
310
+
311
+ const interval = setInterval(async () => {
312
+ try {
313
+ const res = await fetch('/', { method: 'HEAD', cache: 'no-store' });
314
+ if (res.ok) {
315
+ clearInterval(interval);
316
+ retry.classList.add('connected');
317
+ retryText.textContent = 'Server is back! Reconnecting...';
318
+ setTimeout(() => location.reload(), 600);
319
+ }
320
+ } catch {}
321
+ }, 3000);
322
+
323
+ // --- Ambient particles ---
324
+ const container = document.getElementById('particles');
325
+ for (let i = 0; i < 20; i++) {
326
+ const p = document.createElement('span');
327
+ p.style.left = Math.random() * 100 + '%';
328
+ p.style.animationDuration = (12 + Math.random() * 18) + 's';
329
+ p.style.animationDelay = -(Math.random() * 20) + 's';
330
+ p.style.width = p.style.height = (1.5 + Math.random() * 1.5) + 'px';
331
+ container.appendChild(p);
332
+ }
333
+ </script>
334
+ </body>
335
+ </html>
@@ -165,6 +165,8 @@ async function handleStartAction() {
165
165
  if ($label) $label.textContent = t('common.retry');
166
166
  }
167
167
  } catch (err) {
168
+ // Server is unreachable — try protocol handler as fallback
169
+ window.location.href = 'synabun://start';
168
170
  if ($status) $status.textContent = t('loading.somethingWrong');
169
171
  if (btn) btn.disabled = false;
170
172
  if ($label) $label.textContent = t('common.retry');
@@ -1,4 +1,4 @@
1
- const CACHE_NAME = 'synabun-offline-v7';
1
+ const CACHE_NAME = 'synabun-offline-v9';
2
2
  const OFFLINE_URL = '/offline.html';
3
3
 
4
4
  self.addEventListener('install', (event) => {
@@ -20,7 +20,13 @@ self.addEventListener('activate', (event) => {
20
20
  self.addEventListener('fetch', (event) => {
21
21
  if (event.request.mode !== 'navigate') return;
22
22
  event.respondWith(
23
- fetch(event.request).catch(() => caches.match(OFFLINE_URL))
23
+ fetch(event.request).then((response) => {
24
+ // Server is up — silently refresh cached offline.html so it's always current
25
+ caches.open(CACHE_NAME).then((cache) =>
26
+ cache.add(new Request(OFFLINE_URL, { cache: 'reload' }))
27
+ );
28
+ return response;
29
+ }).catch(() => caches.match(OFFLINE_URL))
24
30
  );
25
31
  });
26
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.3.316",
3
+ "version": "2026.3.3102",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",
package/postinstall.js CHANGED
@@ -9,9 +9,10 @@
9
9
  */
10
10
 
11
11
  import { execSync } from 'node:child_process';
12
- import { existsSync } from 'node:fs';
12
+ import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs';
13
13
  import { resolve, dirname } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
+ import { homedir, platform } from 'node:os';
15
16
 
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
18
 
@@ -108,6 +109,90 @@ function buildMcpServer() {
108
109
  }
109
110
  }
110
111
 
112
+ // ── Protocol handler registration ──
113
+
114
+ function registerProtocolHandler() {
115
+ const nodeBin = process.execPath;
116
+ const setupJs = resolve(__dirname, 'setup.js');
117
+ const plat = platform();
118
+
119
+ if (plat === 'win32') {
120
+ // Windows — HKCU registry (no admin required)
121
+ try {
122
+ const cmd = `"${nodeBin}" "${setupJs}" "%1"`;
123
+ execSync(`reg add "HKCU\\Software\\Classes\\synabun" /ve /d "URL:SynaBun Protocol" /f`, { stdio: 'pipe' });
124
+ execSync(`reg add "HKCU\\Software\\Classes\\synabun" /v "URL Protocol" /d "" /f`, { stdio: 'pipe' });
125
+ execSync(`reg add "HKCU\\Software\\Classes\\synabun\\shell\\open\\command" /ve /d "${cmd}" /f`, { stdio: 'pipe' });
126
+ ok('Registered synabun:// protocol handler');
127
+ } catch {
128
+ warn('Could not register synabun:// protocol handler');
129
+ }
130
+ } else if (plat === 'darwin') {
131
+ // macOS — minimal .app bundle in ~/.synabun/
132
+ try {
133
+ const appDir = resolve(homedir(), '.synabun', 'SynaBun.app', 'Contents');
134
+ const macosDir = resolve(appDir, 'MacOS');
135
+ mkdirSync(macosDir, { recursive: true });
136
+
137
+ writeFileSync(resolve(appDir, 'Info.plist'), [
138
+ '<?xml version="1.0" encoding="UTF-8"?>',
139
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
140
+ '<plist version="1.0">',
141
+ '<dict>',
142
+ ' <key>CFBundleIdentifier</key><string>ai.synabun.launcher</string>',
143
+ ' <key>CFBundleName</key><string>SynaBun</string>',
144
+ ' <key>CFBundleExecutable</key><string>synabun-launcher</string>',
145
+ ' <key>CFBundleVersion</key><string>1.0</string>',
146
+ ' <key>CFBundleURLTypes</key>',
147
+ ' <array><dict>',
148
+ ' <key>CFBundleURLName</key><string>SynaBun Protocol</string>',
149
+ ' <key>CFBundleURLSchemes</key><array><string>synabun</string></array>',
150
+ ' </dict></array>',
151
+ '</dict>',
152
+ '</plist>',
153
+ ].join('\n'));
154
+
155
+ const launcherPath = resolve(macosDir, 'synabun-launcher');
156
+ writeFileSync(launcherPath, `#!/bin/sh\nexec "${nodeBin}" "${setupJs}" "$@"\n`);
157
+ chmodSync(launcherPath, 0o755);
158
+
159
+ // Register with LaunchServices
160
+ try {
161
+ execSync(`/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -R "${resolve(homedir(), '.synabun', 'SynaBun.app')}"`, { stdio: 'pipe' });
162
+ } catch {
163
+ try { execSync(`open "${resolve(homedir(), '.synabun', 'SynaBun.app')}"`, { stdio: 'pipe' }); } catch {}
164
+ }
165
+
166
+ ok('Registered synabun:// protocol handler');
167
+ } catch {
168
+ warn('Could not register synabun:// protocol handler');
169
+ }
170
+ } else {
171
+ // Linux — .desktop file
172
+ try {
173
+ const appsDir = resolve(homedir(), '.local', 'share', 'applications');
174
+ mkdirSync(appsDir, { recursive: true });
175
+
176
+ writeFileSync(resolve(appsDir, 'synabun.desktop'), [
177
+ '[Desktop Entry]',
178
+ 'Type=Application',
179
+ 'Name=SynaBun',
180
+ `Exec="${nodeBin}" "${setupJs}" %u`,
181
+ 'MimeType=x-scheme-handler/synabun;',
182
+ 'NoDisplay=true',
183
+ 'Terminal=true',
184
+ '',
185
+ ].join('\n'));
186
+
187
+ try { execSync('xdg-mime default synabun.desktop x-scheme-handler/synabun', { stdio: 'pipe' }); } catch {}
188
+
189
+ ok('Registered synabun:// protocol handler');
190
+ } catch {
191
+ warn('Could not register synabun:// protocol handler');
192
+ }
193
+ }
194
+ }
195
+
111
196
  // ── Main ──
112
197
 
113
198
  function main() {
@@ -117,6 +202,7 @@ function main() {
117
202
  installDeps('MCP Server', resolve(__dirname, 'mcp-server'), { includeDev: needsBuild() });
118
203
  rebuildPty();
119
204
  buildMcpServer();
205
+ registerProtocolHandler();
120
206
 
121
207
  console.log(`\n ${c.green}\u2713${c.reset} Setup complete. Run ${c.cyan}synabun${c.reset} to start.\n`);
122
208
  }
package/preuninstall.js CHANGED
@@ -5,15 +5,20 @@
5
5
  *
6
6
  * Runs before `npm uninstall synabun`.
7
7
  * Removes nested node_modules that cause EPERM/deep-path errors on Windows.
8
+ * Removes the synabun:// protocol handler registration.
8
9
  * Graceful — if cleanup fails, don't block the uninstall.
9
10
  */
10
11
 
11
- import { rmSync } from 'node:fs';
12
+ import { rmSync, existsSync } from 'node:fs';
12
13
  import { resolve, dirname } from 'node:path';
13
14
  import { fileURLToPath } from 'node:url';
15
+ import { execSync } from 'node:child_process';
16
+ import { homedir, platform } from 'node:os';
14
17
 
15
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
19
 
20
+ // ── Remove nested node_modules ──
21
+
17
22
  const dirs = [
18
23
  resolve(__dirname, 'neural-interface', 'node_modules'),
19
24
  resolve(__dirname, 'mcp-server', 'node_modules'),
@@ -26,3 +31,23 @@ for (const dir of dirs) {
26
31
  // Non-fatal — npm will handle remaining cleanup
27
32
  }
28
33
  }
34
+
35
+ // ── Remove protocol handler ──
36
+
37
+ const plat = platform();
38
+
39
+ if (plat === 'win32') {
40
+ try {
41
+ execSync('reg delete "HKCU\\Software\\Classes\\synabun" /f', { stdio: 'pipe' });
42
+ } catch {}
43
+ } else if (plat === 'darwin') {
44
+ const appPath = resolve(homedir(), '.synabun', 'SynaBun.app');
45
+ try {
46
+ rmSync(appPath, { recursive: true, force: true });
47
+ } catch {}
48
+ } else {
49
+ const desktopFile = resolve(homedir(), '.local', 'share', 'applications', 'synabun.desktop');
50
+ try {
51
+ if (existsSync(desktopFile)) rmSync(desktopFile);
52
+ } catch {}
53
+ }