synabun 2026.3.315 → 2026.3.3101

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.
@@ -5617,7 +5617,7 @@ async function init() {
5617
5617
  if (health.projectDir) {
5618
5618
  localStorage.setItem('synabun-project-dir', health.projectDir);
5619
5619
  const cmdEl = document.getElementById('loading-cmd-text');
5620
- if (cmdEl) cmdEl.textContent = 'cd ' + health.projectDir + ' && npm start';
5620
+ if (cmdEl) cmdEl.textContent = 'cd "' + health.projectDir + '" ; npm start';
5621
5621
  }
5622
5622
  if (!health.ok) {
5623
5623
  const messages = {
@@ -5635,7 +5635,7 @@ async function init() {
5635
5635
  const cached = localStorage.getItem('synabun-project-dir');
5636
5636
  if (cached) {
5637
5637
  const cmdEl = document.getElementById('loading-cmd-text');
5638
- if (cmdEl) cmdEl.textContent = 'cd ' + cached + ' && npm start';
5638
+ if (cmdEl) cmdEl.textContent = 'cd "' + cached + '" ; npm start';
5639
5639
  }
5640
5640
  }
5641
5641
 
@@ -5160,7 +5160,7 @@ async function init() {
5160
5160
  if (health.projectDir) {
5161
5161
  localStorage.setItem('synabun-project-dir', health.projectDir);
5162
5162
  const cmdEl = document.getElementById('loading-cmd-text');
5163
- if (cmdEl) cmdEl.textContent = 'cd ' + health.projectDir + ' && npm start';
5163
+ if (cmdEl) cmdEl.textContent = 'cd "' + health.projectDir + '" ; npm start';
5164
5164
  }
5165
5165
  if (!health.ok) {
5166
5166
  const messages = {
@@ -5177,7 +5177,7 @@ async function init() {
5177
5177
  const cached = localStorage.getItem('synabun-project-dir');
5178
5178
  if (cached) {
5179
5179
  const cmdEl = document.getElementById('loading-cmd-text');
5180
- if (cmdEl) cmdEl.textContent = 'cd ' + cached + ' && npm start';
5180
+ if (cmdEl) cmdEl.textContent = 'cd "' + cached + '" ; npm start';
5181
5181
  }
5182
5182
  }
5183
5183
 
@@ -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>
@@ -104,10 +104,8 @@ function resetLoadingToConnecting() {
104
104
  export async function checkHealth() {
105
105
  try {
106
106
  const health = await fetchHealth();
107
- // Cache projectDir for the server-offline command display
108
107
  if (health.projectDir) {
109
108
  localStorage.setItem('synabun-project-dir', health.projectDir);
110
- updateCmdText(health.projectDir);
111
109
  }
112
110
  if (!health.ok) {
113
111
  const messages = {
@@ -130,7 +128,7 @@ export async function checkHealth() {
130
128
  }
131
129
 
132
130
  /**
133
- * Update the command text element with the full path to npm start.
131
+ * Update the command text element with the restart command.
134
132
  */
135
133
  function updateCmdText(projectDir) {
136
134
  const cmdEl = $('loading-cmd-text');
@@ -167,6 +165,8 @@ async function handleStartAction() {
167
165
  if ($label) $label.textContent = t('common.retry');
168
166
  }
169
167
  } catch (err) {
168
+ // Server is unreachable — try protocol handler as fallback
169
+ window.location.href = 'synabun://start';
170
170
  if ($status) $status.textContent = t('loading.somethingWrong');
171
171
  if (btn) btn.disabled = false;
172
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.315",
3
+ "version": "2026.3.3101",
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
+ }