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.
- package/README.md +64 -5
- package/cli.js +53 -4
- package/package.json +3 -2
- package/public/css/core/responsive.css +2 -2
- package/public/css/ui/file-picker.css +243 -17
- package/public/css/ui/messages.css +72 -9
- package/public/css/ui/toolbox.css +43 -0
- package/public/index.html +80 -745
- package/public/js/components/add-project-modal.js +27 -0
- package/public/js/components/agent-modal.js +73 -0
- package/public/js/components/agent-monitor-modal.js +19 -0
- package/public/js/components/bg-confirm-modal.js +22 -0
- package/public/js/components/chain-modal.js +52 -0
- package/public/js/components/cost-dashboard-modal.js +39 -0
- package/public/js/components/dag-editor-modal.js +55 -0
- package/public/js/components/file-picker-modal.js +45 -0
- package/public/js/components/linear-create-modal.js +43 -0
- package/public/js/components/mcp-modal.js +58 -0
- package/public/js/components/orchestrate-modal.js +40 -0
- package/public/js/components/permission-modal.js +30 -0
- package/public/js/components/prompt-modal.js +31 -0
- package/public/js/components/shortcuts-modal.js +45 -0
- package/public/js/components/status-bar.js +97 -0
- package/public/js/components/system-prompt-modal.js +29 -0
- package/public/js/components/telegram-modal.js +84 -0
- package/public/js/components/welcome-overlay.js +60 -0
- package/public/js/components/workflow-modal.js +41 -0
- package/public/js/core/api.js +10 -0
- package/public/js/core/dom.js +3 -2
- package/public/js/core/ws.js +7 -1
- package/public/js/features/attachments.js +226 -23
- package/public/js/features/projects.js +7 -0
- package/public/js/main.js +22 -0
- package/public/js/ui/shortcuts.js +4 -8
- package/public/login.html +470 -0
- package/public/offline.html +300 -168
- package/public/sw.js +10 -2
- package/server/agent-loop.js +1 -0
- package/server/auth.js +141 -0
- package/server/orchestrator.js +1 -0
- package/server/ws-handler.js +2 -0
- package/server.js +14 -3
package/public/offline.html
CHANGED
|
@@ -1,190 +1,322 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en"
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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">></span>
|
|
254
|
+
<span class="term-cmd">claude</span>
|
|
255
|
+
<span class="term-sep">·</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
|
-
|
|
132
|
-
|
|
133
|
-
<div class="
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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).
|
|
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
|
}
|
package/server/agent-loop.js
CHANGED
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
|
+
}
|