claudeck 1.2.0 → 1.3.1

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 (42) hide show
  1. package/README.md +64 -5
  2. package/cli.js +53 -4
  3. package/package.json +3 -2
  4. package/public/css/core/responsive.css +2 -2
  5. package/public/css/ui/file-picker.css +243 -17
  6. package/public/css/ui/messages.css +72 -9
  7. package/public/css/ui/toolbox.css +43 -0
  8. package/public/index.html +80 -745
  9. package/public/js/components/add-project-modal.js +27 -0
  10. package/public/js/components/agent-modal.js +73 -0
  11. package/public/js/components/agent-monitor-modal.js +19 -0
  12. package/public/js/components/bg-confirm-modal.js +22 -0
  13. package/public/js/components/chain-modal.js +52 -0
  14. package/public/js/components/cost-dashboard-modal.js +39 -0
  15. package/public/js/components/dag-editor-modal.js +55 -0
  16. package/public/js/components/file-picker-modal.js +45 -0
  17. package/public/js/components/linear-create-modal.js +43 -0
  18. package/public/js/components/mcp-modal.js +58 -0
  19. package/public/js/components/orchestrate-modal.js +40 -0
  20. package/public/js/components/permission-modal.js +30 -0
  21. package/public/js/components/prompt-modal.js +31 -0
  22. package/public/js/components/shortcuts-modal.js +45 -0
  23. package/public/js/components/status-bar.js +97 -0
  24. package/public/js/components/system-prompt-modal.js +29 -0
  25. package/public/js/components/telegram-modal.js +84 -0
  26. package/public/js/components/welcome-overlay.js +60 -0
  27. package/public/js/components/workflow-modal.js +41 -0
  28. package/public/js/core/api.js +10 -0
  29. package/public/js/core/dom.js +3 -2
  30. package/public/js/core/ws.js +7 -1
  31. package/public/js/features/attachments.js +226 -23
  32. package/public/js/features/projects.js +7 -0
  33. package/public/js/main.js +22 -0
  34. package/public/js/ui/shortcuts.js +4 -8
  35. package/public/login.html +470 -0
  36. package/public/offline.html +300 -168
  37. package/public/sw.js +10 -2
  38. package/server/agent-loop.js +1 -0
  39. package/server/auth.js +141 -0
  40. package/server/orchestrator.js +1 -0
  41. package/server/ws-handler.js +2 -0
  42. package/server.js +14 -3
@@ -1,190 +1,322 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" dir="ltr">
2
+ <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="theme-color" content="#0d1117">
7
- <title>Claudeck :: offline</title>
8
- <style>
9
- * { margin: 0; padding: 0; box-sizing: border-box; }
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="theme-color" content="#020203">
7
+ <meta name="apple-mobile-web-app-capable" content="yes">
8
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9
+ <title>Claudeck :: offline</title>
10
+ <link rel="icon" type="image/png" href="/icons/favicon.png">
11
+ <link rel="preconnect" href="https://fonts.googleapis.com">
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
+ <link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Outfit:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
14
+ <style>
15
+ *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
10
16
 
11
- body {
12
- background: #050508;
13
- color: #b8c4d0;
14
- font-family: "SF Mono", "Fira Code", "JetBrains Mono", "Cascadia Code", "Consolas", monospace;
15
- display: flex;
16
- align-items: center;
17
- justify-content: center;
18
- min-height: 100vh;
19
- overflow: hidden;
20
- }
17
+ :root{
18
+ --bg: #050508;
19
+ --bg-secondary: #0c0d10;
20
+ --bg-deep: #020203;
21
+ --bg-elevated: #181a22;
22
+ --border: #1e2028;
23
+ --border-subtle: #161820;
24
+ --border-accent: rgba(51,209,122,.12);
25
+ --text: #c4cfd9;
26
+ --text-sec: #6b7a8d;
27
+ --text-dim: #3a4250;
28
+ --accent: #33d17a;
29
+ --accent-dim: rgba(51,209,122,.08);
30
+ --accent-glow: rgba(51,209,122,.25);
31
+ --success: #33d17a;
32
+ --warning: #e5a50a;
33
+ --error: #ed333b;
34
+ --font-display: "Chakra Petch","Rajdhani","Exo 2",sans-serif;
35
+ --font-sans: "Outfit","DM Sans","Segoe UI",sans-serif;
36
+ --font-mono: "JetBrains Mono","SF Mono","Fira Code","Cascadia Code","Consolas",monospace;
37
+ --header-h: 40px;
38
+ }
21
39
 
22
- .offline-container {
23
- text-align: center;
24
- padding: 2rem;
25
- max-width: 480px;
26
- }
40
+ /* Light theme */
41
+ html[data-theme="light"]{
42
+ --bg: #f7f7f4;
43
+ --bg-secondary: #eeeee9;
44
+ --bg-deep: #eaeae4;
45
+ --bg-elevated: #ffffff;
46
+ --border: #c8c8c0;
47
+ --border-subtle: #d6d6cf;
48
+ --border-accent: rgba(26,138,74,.12);
49
+ --text: #1a1a18;
50
+ --text-sec: #4a4a44;
51
+ --text-dim: #8a8a80;
52
+ --accent: #1a8a4a;
53
+ --accent-dim: rgba(26,138,74,.06);
54
+ --accent-glow: rgba(26,138,74,.15);
55
+ --success: #1a8a4a;
56
+ --warning: #8a6500;
57
+ --error: #c41a22;
58
+ }
59
+ html[data-theme="light"] .top-header::after,
60
+ html[data-theme="light"] .status-bar::before{
61
+ opacity:0;
62
+ }
27
63
 
28
- /* ASCII art logo */
29
- .ascii-logo {
30
- margin: 0 auto 2rem;
31
- font-size: 3.2px;
32
- line-height: 1.15;
33
- letter-spacing: 0.02em;
34
- color: #33d17a;
35
- white-space: pre;
36
- overflow: hidden;
37
- text-align: left;
38
- display: inline-block;
39
- text-shadow: 0 0 6px rgba(51, 209, 122, 0.3);
40
- }
64
+ html{height:100%}
65
+ body{
66
+ background:var(--bg);
67
+ color:var(--text);
68
+ font-family:var(--font-sans);
69
+ min-height:100vh;
70
+ display:flex;
71
+ flex-direction:column;
72
+ }
41
73
 
42
- @media (min-width: 480px) {
43
- .ascii-logo { font-size: 4.5px; }
44
- }
45
- @media (min-width: 640px) {
46
- .ascii-logo { font-size: 5.5px; }
47
- }
74
+ /* ── Header ── */
75
+ .top-header{
76
+ height:var(--header-h);
77
+ background:var(--bg-deep);
78
+ border-bottom:1px solid var(--border);
79
+ display:flex;
80
+ align-items:center;
81
+ padding:0 16px;
82
+ font-family:var(--font-mono);
83
+ font-size:12px;
84
+ gap:8px;
85
+ flex-shrink:0;
86
+ position:relative;
87
+ }
88
+ .top-header::after{
89
+ content:"";
90
+ position:absolute;
91
+ bottom:-1px;left:0;right:0;
92
+ height:1px;
93
+ background:linear-gradient(90deg, transparent, var(--accent-glow), transparent);
94
+ opacity:.3;
95
+ }
96
+ .term-prompt{ color:var(--success); font-weight:700; }
97
+ .term-cmd{ color:var(--accent); font-weight:600; font-family:var(--font-display); letter-spacing:.04em; }
98
+ .term-sep{ color:var(--border); margin:0 2px; }
99
+ .term-status{ color:var(--text-dim); font-size:11px; }
100
+ .header-right{
101
+ margin-left:auto;
102
+ display:flex;align-items:center;gap:6px;
103
+ color:var(--error);
104
+ font-size:11px;
105
+ }
106
+ .header-right svg{ color:var(--error); }
48
107
 
49
- .offline-title {
50
- font-size: 1.4rem;
51
- color: #33d17a;
52
- margin-bottom: 0.5rem;
53
- direction: ltr;
54
- }
108
+ /* ── Main ── */
109
+ .main-area{
110
+ flex:1;
111
+ display:flex;
112
+ flex-direction:column;
113
+ align-items:center;
114
+ justify-content:center;
115
+ padding:40px 24px 80px;
116
+ gap:16px;
117
+ opacity:0;
118
+ animation:fadeIn .5s ease .1s forwards;
119
+ }
120
+ @keyframes fadeIn{to{opacity:1}}
55
121
 
122
+ .whaly-img{
123
+ width:120px;
124
+ height:auto;
125
+ image-rendering:pixelated;
126
+ filter:drop-shadow(0 8px 24px rgba(51,209,122,.08)) grayscale(.6);
127
+ transition:filter .3s;
128
+ cursor:pointer;
129
+ }
130
+ .whaly-img:hover{
131
+ filter:drop-shadow(0 8px 32px rgba(51,209,122,.15)) grayscale(.3);
132
+ }
56
133
 
57
- .offline-message {
58
- color: #6b7a8d;
59
- font-size: 0.85rem;
60
- line-height: 1.6;
61
- margin-bottom: 2rem;
62
- }
134
+ .whaly-text{
135
+ color:var(--text-dim);
136
+ font-size:14px;
137
+ font-family:var(--font-display);
138
+ text-align:center;
139
+ letter-spacing:.04em;
140
+ font-weight:500;
141
+ }
142
+ .whaly-hint{
143
+ color:var(--text-dim);
144
+ font-size:11px;
145
+ font-family:var(--font-sans);
146
+ opacity:.5;
147
+ text-align:center;
148
+ line-height:1.6;
149
+ }
63
150
 
64
- .retry-btn {
65
- background: transparent;
66
- border: 1px solid #33d17a;
67
- color: #33d17a;
68
- padding: 0.6rem 1.8rem;
69
- font-family: inherit;
70
- font-size: 0.85rem;
71
- cursor: pointer;
72
- border-radius: 4px;
73
- transition: all 0.2s;
74
- }
151
+ /* ── Retry button — styled like a ghost input bar ── */
152
+ .retry-wrap{
153
+ margin-top:12px;
154
+ opacity:0;
155
+ animation:fadeIn .4s ease .4s forwards;
156
+ }
157
+ .retry-btn{
158
+ background:var(--bg-secondary);
159
+ border:1px solid var(--border);
160
+ color:var(--accent);
161
+ font-family:var(--font-mono);
162
+ font-size:12px;
163
+ font-weight:500;
164
+ padding:10px 24px;
165
+ border-radius:8px;
166
+ cursor:pointer;
167
+ display:inline-flex;
168
+ align-items:center;
169
+ gap:8px;
170
+ transition:all .2s;
171
+ }
172
+ .retry-btn:hover{
173
+ border-color:var(--accent);
174
+ box-shadow:0 0 0 1px var(--border-accent);
175
+ color:var(--accent);
176
+ }
177
+ .retry-btn:active{
178
+ transform:scale(.98);
179
+ }
180
+ .retry-btn svg{
181
+ width:14px;height:14px;
182
+ animation:spin 2s linear infinite paused;
183
+ }
184
+ .retry-btn:hover svg{
185
+ animation-play-state:running;
186
+ }
187
+ @keyframes spin{
188
+ to{transform:rotate(360deg)}
189
+ }
75
190
 
76
- .retry-btn:hover {
77
- background: rgba(51, 209, 122, 0.1);
78
- box-shadow: 0 0 12px rgba(51, 209, 122, 0.2);
79
- }
80
-
81
- /* Subtle glow pulse on ASCII logo */
82
- @keyframes ascii-glow {
83
- 0%, 100% { text-shadow: 0 0 6px rgba(51, 209, 122, 0.3); }
84
- 50% { text-shadow: 0 0 14px rgba(51, 209, 122, 0.5), 0 0 30px rgba(51, 209, 122, 0.15); }
85
- }
86
-
87
- .ascii-logo { animation: ascii-glow 4s ease-in-out infinite; }
88
-
89
- /* Floating geometric particles */
90
- .particles {
91
- position: fixed;
92
- inset: 0;
93
- pointer-events: none;
94
- z-index: -1;
95
- }
96
-
97
- .particle {
98
- position: absolute;
99
- opacity: 0.06;
100
- animation: float 20s infinite linear;
101
- }
102
-
103
- @keyframes float {
104
- 0% { transform: translateY(100vh) rotate(0deg); }
105
- 100% { transform: translateY(-20vh) rotate(360deg); }
106
- }
107
- </style>
191
+ /* ── Status bar ── */
192
+ .status-bar{
193
+ height:24px;
194
+ background:var(--bg-deep);
195
+ border-top:1px solid var(--border);
196
+ display:flex;
197
+ align-items:center;
198
+ padding:0 12px;
199
+ font-family:var(--font-mono);
200
+ font-size:11px;
201
+ color:var(--text-dim);
202
+ flex-shrink:0;
203
+ user-select:none;
204
+ position:relative;
205
+ }
206
+ .status-bar::before{
207
+ content:"";
208
+ position:absolute;
209
+ top:-1px;left:0;right:0;
210
+ height:1px;
211
+ background:linear-gradient(90deg, transparent, var(--accent-glow), transparent);
212
+ opacity:.3;
213
+ }
214
+ .sb-left,.sb-right{
215
+ display:flex;align-items:center;gap:0;
216
+ }
217
+ .sb-left{flex:1;justify-content:flex-start;}
218
+ .sb-right{flex:1;justify-content:flex-end;}
219
+ .sb-item{
220
+ display:inline-flex;align-items:center;gap:4px;
221
+ padding:0 8px;height:24px;white-space:nowrap;
222
+ }
223
+ .sb-dot{
224
+ width:6px;height:6px;border-radius:50%;
225
+ background:var(--error);flex-shrink:0;
226
+ }
227
+ .sb-dot.blink{
228
+ animation:blink 1.5s infinite;
229
+ }
230
+ @keyframes blink{
231
+ 0%,100%{opacity:1}
232
+ 50%{opacity:.3}
233
+ }
234
+ .sb-sep{
235
+ width:1px;height:12px;
236
+ background:var(--border);flex-shrink:0;
237
+ }
108
238
 
239
+ /* ── Responsive ── */
240
+ @media(max-width:640px){
241
+ .whaly-img{width:80px}
242
+ .header-right span{display:none}
243
+ }
244
+ </style>
245
+ <script>
246
+ const t = localStorage.getItem('claudeck-theme');
247
+ if (t) document.documentElement.setAttribute('data-theme', t);
248
+ </script>
109
249
  </head>
110
250
  <body>
111
- <!-- Floating geometric particles background -->
112
- <div class="particles">
113
- <svg class="particle" style="left:10%;width:30px;animation-delay:0s;animation-duration:25s" viewBox="0 0 40 40">
114
- <polygon points="20,2 38,20 20,38 2,20" fill="none" stroke="#33d17a" stroke-width="1"/>
115
- </svg>
116
- <svg class="particle" style="left:30%;width:20px;animation-delay:5s;animation-duration:18s" viewBox="0 0 40 40">
117
- <polygon points="20,0 40,20 20,40 0,20" fill="none" stroke="#c69ff5" stroke-width="1"/>
118
- </svg>
119
- <svg class="particle" style="left:55%;width:25px;animation-delay:2s;animation-duration:22s" viewBox="0 0 40 40">
120
- <circle cx="20" cy="20" r="15" fill="none" stroke="#33d17a" stroke-width="1"/>
121
- </svg>
122
- <svg class="particle" style="left:75%;width:35px;animation-delay:8s;animation-duration:28s" viewBox="0 0 60 60">
123
- <path d="M30,5 L55,30 L30,55 L5,30 Z" fill="none" stroke="#c69ff5" stroke-width="1"/>
124
- <path d="M30,15 L45,30 L30,45 L15,30 Z" fill="none" stroke="#33d17a" stroke-width="1"/>
125
- </svg>
126
- <svg class="particle" style="left:90%;width:18px;animation-delay:12s;animation-duration:20s" viewBox="0 0 40 40">
127
- <polygon points="20,2 38,20 20,38 2,20" fill="none" stroke="#33d17a" stroke-width="1"/>
251
+
252
+ <header class="top-header">
253
+ <span class="term-prompt">&gt;</span>
254
+ <span class="term-cmd">claude</span>
255
+ <span class="term-sep">&middot;</span>
256
+ <span class="term-status">offline</span>
257
+ <div class="header-right">
258
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
259
+ <line x1="1" y1="1" x2="23" y2="23"/><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/><path d="M10.71 5.05A16 16 0 0 1 22.56 9"/><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/>
128
260
  </svg>
261
+ <span>disconnected</span>
262
+ </div>
263
+ </header>
264
+
265
+ <main class="main-area">
266
+ <img src="/icons/whaly.png" alt="Whaly" class="whaly-img" id="whaly">
267
+ <div class="whaly-text">~ no connection ~</div>
268
+ <div class="whaly-hint">
269
+ Claudeck needs a network connection to<br>
270
+ communicate with Claude Code
129
271
  </div>
272
+ <div class="retry-wrap">
273
+ <button class="retry-btn" onclick="location.reload()">
274
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
275
+ <polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
276
+ </svg>
277
+ retry connection
278
+ </button>
279
+ </div>
280
+ </main>
130
281
 
131
- <div class="offline-container">
132
- <!-- ASCII falcon logo -->
133
- <div class="ascii-logo">
134
- -%%%%%%%%%%%##**++==---::....
135
- *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%##*+=--::...
136
- #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*######******##*****+
137
- .#@%**+++**##%%@@@@@@@@@@@@@@@@@@%#*#*******************:
138
- .#@#*+.-*######%%%%#**+++++=+#@@%%#*#####***************+
139
- .#@#*=:=***####%%%%%%%%%%%%%#%@@%##*####*****************:
140
- .%@#*--+****######%%%%%%%%%%%@@@%##*#####***************+:
141
- :%@##-=+****########%%%%%%%%%@@@%##*####****************+.
142
- :%@#*-===+++**########%%%%%%@@@@%#**#####**************+*.
143
- :@@#*-====++++**###########%%@@@%#*######**************+*
144
- -@@#*--====+++++++*****#####%@@@%#*#######*************++
145
- -@@#*--======++++++++++*****#@@%%#*#####***************++
146
- -@%#*--========+++++++++++++#@@%%#*######**************++
147
- =@%#*--=================+===#@@%%#*##*****************++=
148
- =@%#*---====================#@@%#**#####*#************++=
149
- =@%#*----===================%@@%#*########************++-
150
- +@%%@#*+====================@@@%#*######*************+++-
151
- +@@@@@@@@@%%*==============-@@@%#*###*#**************+++:
152
- +@@@@@@@@@@@@@@@@@@@#+======@@@%#**##****************+++:
153
- *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%***#****************++++.
154
- *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%********************++++.
155
- *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%*******************+++++
156
- *@@@@@@@@@@@@@##@@@@@@@@@@@@@@%#******************+++++=
157
- #@%%@@@@@@@@@@@@@%#-:=*#%%@@@@%#*****************++++++=
158
- .#@@@@@@@@@@@@@@@@@@@@@%=::*@@@%#*****************++++++=
159
- +##%%@@@@@@@@@@@@@@@@@@@@@@@@@%*****************++++++=-
160
- .*%#*#%##: .:+####%%%@@@@@@@@@@@@@@@@@@%*****************+++=+==-
161
- .:*@%##+**++%%#%%*-:.-++===**##%%%@@@@@@@@@@@@@%***************+++=+=+==:
162
- -*%%%##%*#+*****+#**##%%%#*=======++*##%%%%@@@@@%#************++*++++++-.
163
- -%@@#%#**+#+##+**=#*+#*+#+##+#%@@#+=========+####%%##*********+++++++++:.
164
- .=%@@@@##*+*#+##*+#+##***+**+**+#*+#**#%%@#++======-==+*=--=****++*+**+=: .:.
165
- .*@@@@@@@%*##*###**+#***#+*#**#*+#***#=*+*#**%%%%#*+=====::++*****+***-: :.
166
- +%@@@@@@@@@@########**#+#*+**+#*+*#*+#*+##+**+####%@@@#====++*****: ::
167
- -*##%@@@@@@@@@%########*+#+*#*+*#+#*****=#*+*#****%@@@@@@#-=+=: :-
168
- .:=*#%%@@@@@@@@@@%#########+*+##*+**+###******%@@@@@@%#*:...... ::
169
- .*##%%@@@@@@@@@@%###*+####+*%%####****#@@@@@@@##**=--=-==++++. .-=.
170
- .-+#%%@@@@@@@@@@@%###*+##*=+##***%@@@@@@%#**+-.:.. ..:-==: .:--:.
171
- .-*#%%@@@@@@@@@@@%##**%%%%%@@@@@@@%#**+: .:=#**==-+---:.:----::::.
172
- :+##%%@@@@@@@@@@@@@@@@@@@@@@##**+-. :#@#####***+--=.
173
- .-+#%%%@@@@@@@@@@@@@@@%#**+-. .+@@@@@@@@%%%@@@%-
174
- .:*##%%@@@@@@@@@%#**+: .=%@@@@@@@@@@@@@@##*=
175
- .-*##%%@@@#**+=. .*@@@@@@@@@@@@@@%#*****.
176
- :=*##**+-. =@@@@@@@@@@@@@%#****+-.
177
- .::. +@@@@@@@@@@@#****+-.
178
- :+#%@@@@@#***+=.
282
+ <div class="status-bar">
283
+ <div class="sb-left">
284
+ <div class="sb-item">
285
+ <div class="sb-dot blink"></div>
286
+ disconnected
287
+ </div>
288
+ <div class="sb-sep"></div>
289
+ <div class="sb-item">waiting for network</div>
290
+ </div>
291
+ <div class="sb-right">
292
+ <div class="sb-item">offline</div>
293
+ </div>
179
294
  </div>
180
295
 
181
- <div class="offline-title">> offline</div>
296
+ <script>
297
+ // Auto-retry every 10 seconds
298
+ let retryTimer;
299
+ function startAutoRetry() {
300
+ retryTimer = setInterval(() => {
301
+ fetch('/api/version', { cache: 'no-store' })
302
+ .then(r => { if (r.ok) location.reload(); })
303
+ .catch(() => {});
304
+ }, 10000);
305
+ }
306
+ startAutoRetry();
182
307
 
183
- <p class="offline-message">
184
- No network connection available.<br>
185
- Claudeck requires a connection to communicate with Claude.
186
- </p>
187
- <button class="retry-btn" onclick="location.reload()">retry connection</button>
188
- </div>
308
+ // Whaly easter egg
309
+ let clicks = 0;
310
+ document.getElementById('whaly').addEventListener('click', () => {
311
+ clicks++;
312
+ if (clicks >= 5) {
313
+ clicks = 0;
314
+ const w = document.getElementById('whaly');
315
+ w.style.transition = 'transform .4s cubic-bezier(.34,1.56,.64,1)';
316
+ w.style.transform = 'scale(1.2) rotate(-10deg)';
317
+ setTimeout(() => { w.style.transform = ''; }, 500);
318
+ }
319
+ });
320
+ </script>
189
321
  </body>
190
322
  </html>
package/public/sw.js CHANGED
@@ -4,10 +4,13 @@ const CACHE_NAME = 'claudeck-v1';
4
4
  const OFFLINE_URL = '/offline.html';
5
5
 
6
6
  // Assets to pre-cache for offline support
7
+ const LOGIN_URL = '/login';
7
8
  const PRECACHE_URLS = [
8
9
  OFFLINE_URL,
10
+ '/login.html',
9
11
  '/icons/icon-192.png',
10
12
  '/icons/icon-512.png',
13
+ '/icons/whaly.png',
11
14
  ];
12
15
 
13
16
  // ── Install: pre-cache offline page ──
@@ -32,10 +35,15 @@ self.addEventListener('fetch', (event) => {
32
35
  // Only handle GET requests
33
36
  if (event.request.method !== 'GET') return;
34
37
 
35
- // Navigation requests (HTML pages) — show offline page on failure
38
+ // Navigation requests (HTML pages) — redirect to login on 401, show offline page on failure
36
39
  if (event.request.mode === 'navigate') {
37
40
  event.respondWith(
38
- fetch(event.request).catch(() => caches.match(OFFLINE_URL))
41
+ fetch(event.request).then((response) => {
42
+ if (response.status === 401) {
43
+ return Response.redirect(LOGIN_URL, 302);
44
+ }
45
+ return response;
46
+ }).catch(() => caches.match(OFFLINE_URL))
39
47
  );
40
48
  return;
41
49
  }
@@ -145,6 +145,7 @@ export async function runAgent({
145
145
  abortController,
146
146
  maxTurns,
147
147
  executable: execPath,
148
+ settingSources: ["user", "project", "local"],
148
149
  };
149
150
 
150
151
  if (!useBypass && !usePlan) {
package/server/auth.js ADDED
@@ -0,0 +1,141 @@
1
+ // Token-based authentication for Claudeck
2
+ // Zero external dependencies — uses Node.js built-in crypto
3
+ import crypto from "crypto";
4
+
5
+ export function generateToken() {
6
+ return crypto.randomBytes(32).toString("hex");
7
+ }
8
+
9
+ export function getToken() {
10
+ return process.env.CLAUDECK_TOKEN || null;
11
+ }
12
+
13
+ export function isAuthEnabled() {
14
+ if (process.env.CLAUDECK_AUTH === "false") return false;
15
+ if (process.env.CLAUDECK_AUTH === "true") return true;
16
+ if (process.env.CLAUDECK_TOKEN) return true;
17
+ return false;
18
+ }
19
+
20
+ export function parseCookies(cookieHeader) {
21
+ const cookies = {};
22
+ if (!cookieHeader) return cookies;
23
+ for (const pair of cookieHeader.split(";")) {
24
+ const idx = pair.indexOf("=");
25
+ if (idx < 0) continue;
26
+ const key = pair.slice(0, idx).trim();
27
+ const val = pair.slice(idx + 1).trim();
28
+ cookies[key] = val;
29
+ }
30
+ return cookies;
31
+ }
32
+
33
+ export function validateToken(candidate) {
34
+ const stored = getToken();
35
+ if (!stored || !candidate) return false;
36
+ try {
37
+ const a = Buffer.from(String(candidate));
38
+ const b = Buffer.from(String(stored));
39
+ if (a.length !== b.length) return false;
40
+ return crypto.timingSafeEqual(a, b);
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ export function extractToken(req) {
47
+ // 1. Authorization: Bearer <token>
48
+ const authHeader = req.headers.authorization;
49
+ if (authHeader && authHeader.startsWith("Bearer ")) {
50
+ return authHeader.slice(7);
51
+ }
52
+ // 2. Cookie
53
+ const cookies = parseCookies(req.headers.cookie);
54
+ if (cookies.claudeck_token) {
55
+ return cookies.claudeck_token;
56
+ }
57
+ // 3. Query parameter (for edge cases)
58
+ const url = new URL(req.url, "http://localhost");
59
+ const qToken = url.searchParams.get("token");
60
+ if (qToken) return qToken;
61
+
62
+ return null;
63
+ }
64
+
65
+ function isLocalhost(req) {
66
+ // If request has proxy headers, it's being tunneled — not truly local
67
+ if (req.headers["x-forwarded-for"] || req.headers["x-real-ip"]) return false;
68
+ const addr = req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || "";
69
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
70
+ }
71
+
72
+ export function authMiddleware(req, res, next) {
73
+ if (!isAuthEnabled()) return next();
74
+
75
+ // Localhost bypass (default on, set CLAUDECK_AUTH_LOCALHOST=true to require auth even on localhost)
76
+ if (process.env.CLAUDECK_AUTH_LOCALHOST !== "true" && isLocalhost(req)) {
77
+ return next();
78
+ }
79
+
80
+ const token = extractToken(req);
81
+ if (token && validateToken(token)) return next();
82
+
83
+ // For page navigations, redirect to login
84
+ const accept = req.headers.accept || "";
85
+ if (accept.includes("text/html")) {
86
+ return res.redirect("/login");
87
+ }
88
+
89
+ // For API/asset requests, return 401
90
+ return res.status(401).json({ error: "Unauthorized" });
91
+ }
92
+
93
+ export function verifyWsClient(info, callback) {
94
+ if (!isAuthEnabled()) return callback(true);
95
+
96
+ // Localhost bypass (skip if proxied)
97
+ const addr = info.req.socket?.remoteAddress || "";
98
+ const isProxied = info.req.headers["x-forwarded-for"] || info.req.headers["x-real-ip"];
99
+ if (process.env.CLAUDECK_AUTH_LOCALHOST !== "true" && !isProxied) {
100
+ if (addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1") {
101
+ return callback(true);
102
+ }
103
+ }
104
+
105
+ // Check cookie on the upgrade request
106
+ const cookies = parseCookies(info.req.headers.cookie);
107
+ if (cookies.claudeck_token && validateToken(cookies.claudeck_token)) {
108
+ return callback(true);
109
+ }
110
+
111
+ // Check query string (ws://host/ws?token=xxx)
112
+ try {
113
+ const url = new URL(info.req.url, "http://localhost");
114
+ const qToken = url.searchParams.get("token");
115
+ if (qToken && validateToken(qToken)) {
116
+ return callback(true);
117
+ }
118
+ } catch {}
119
+
120
+ callback(false, 401, "Unauthorized");
121
+ }
122
+
123
+ export function loginHandler(req, res) {
124
+ if (!isAuthEnabled()) return res.json({ ok: true });
125
+ const { token } = req.body || {};
126
+ if (!validateToken(token)) {
127
+ return res.status(401).json({ error: "Invalid token" });
128
+ }
129
+ res.cookie("claudeck_token", token, {
130
+ httpOnly: true,
131
+ sameSite: "strict",
132
+ path: "/",
133
+ maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year
134
+ secure: req.protocol === "https",
135
+ });
136
+ res.json({ ok: true });
137
+ }
138
+
139
+ export function statusHandler(_req, res) {
140
+ res.json({ authEnabled: isAuthEnabled() });
141
+ }