@chrysb/alphaclaw 0.1.9 → 0.1.11
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/bin/alphaclaw.js +24 -0
- package/lib/public/css/shell.css +135 -0
- package/lib/public/css/theme.css +49 -0
- package/lib/public/img/logo.svg +3 -0
- package/lib/public/js/app.js +54 -55
- package/lib/public/js/components/credentials-modal.js +63 -59
- package/lib/public/js/components/envars.js +67 -68
- package/lib/public/js/components/google.js +2 -2
- package/lib/public/js/components/welcome.js +12 -11
- package/lib/public/login.html +126 -81
- package/lib/public/setup.html +11 -13
- package/lib/server/alphaclaw-version.js +10 -4
- package/lib/server/constants.js +3 -1
- package/package.json +1 -1
package/bin/alphaclaw.js
CHANGED
|
@@ -67,6 +67,30 @@ const openclawDir = path.join(rootDir, ".openclaw");
|
|
|
67
67
|
fs.mkdirSync(openclawDir, { recursive: true });
|
|
68
68
|
console.log(`[alphaclaw] Root directory: ${rootDir}`);
|
|
69
69
|
|
|
70
|
+
// Check for pending update marker (written by the update endpoint before restart).
|
|
71
|
+
// In environments where the container filesystem is ephemeral (Railway, etc.),
|
|
72
|
+
// the npm install from the update endpoint is lost on restart. This re-runs it
|
|
73
|
+
// from the fresh container using the persistent volume marker.
|
|
74
|
+
const pendingUpdateMarker = path.join(rootDir, ".alphaclaw-update-pending");
|
|
75
|
+
if (fs.existsSync(pendingUpdateMarker)) {
|
|
76
|
+
console.log("[alphaclaw] Pending update detected, installing @chrysb/alphaclaw@latest...");
|
|
77
|
+
const alphaPkgRoot = path.resolve(__dirname, "..");
|
|
78
|
+
const nmIndex = alphaPkgRoot.lastIndexOf(`${path.sep}node_modules${path.sep}`);
|
|
79
|
+
const installDir = nmIndex >= 0 ? alphaPkgRoot.slice(0, nmIndex) : alphaPkgRoot;
|
|
80
|
+
try {
|
|
81
|
+
execSync("npm install @chrysb/alphaclaw@latest --omit=dev --prefer-online", {
|
|
82
|
+
cwd: installDir,
|
|
83
|
+
stdio: "inherit",
|
|
84
|
+
timeout: 180000,
|
|
85
|
+
});
|
|
86
|
+
fs.unlinkSync(pendingUpdateMarker);
|
|
87
|
+
console.log("[alphaclaw] Update applied successfully");
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.log(`[alphaclaw] Update install failed: ${e.message}`);
|
|
90
|
+
fs.unlinkSync(pendingUpdateMarker);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
70
94
|
// ---------------------------------------------------------------------------
|
|
71
95
|
// 3. Symlink ~/.openclaw -> <root>/.openclaw
|
|
72
96
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* ── App shell grid ─────────────────────────────── */
|
|
2
|
+
|
|
3
|
+
.app-shell {
|
|
4
|
+
display: grid;
|
|
5
|
+
grid-template-columns: 220px 1fr;
|
|
6
|
+
grid-template-rows: 1fr 24px;
|
|
7
|
+
height: 100vh;
|
|
8
|
+
position: relative;
|
|
9
|
+
z-index: 1;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.app-content {
|
|
13
|
+
overflow-y: auto;
|
|
14
|
+
padding: 24px 32px;
|
|
15
|
+
position: relative;
|
|
16
|
+
z-index: 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ── Sidebar ───────────────────────────────────── */
|
|
20
|
+
|
|
21
|
+
.app-sidebar {
|
|
22
|
+
background: var(--bg-sidebar);
|
|
23
|
+
border-right: 1px solid var(--border);
|
|
24
|
+
overflow-y: auto;
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.sidebar-brand {
|
|
30
|
+
padding: 16px;
|
|
31
|
+
font-size: 14px;
|
|
32
|
+
letter-spacing: 0.03em;
|
|
33
|
+
color: var(--text-muted);
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 8px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.sidebar-label {
|
|
40
|
+
padding: 12px 16px 6px;
|
|
41
|
+
font-size: 11px;
|
|
42
|
+
font-weight: 700;
|
|
43
|
+
color: var(--text-dim);
|
|
44
|
+
text-transform: uppercase;
|
|
45
|
+
letter-spacing: 0.1em;
|
|
46
|
+
user-select: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.sidebar-nav { list-style: none; }
|
|
50
|
+
|
|
51
|
+
.sidebar-nav a {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 8px;
|
|
55
|
+
padding: 6px 16px 6px 24px;
|
|
56
|
+
color: var(--text-muted);
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
font-size: 13px;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
transition: background 0.1s, color 0.1s;
|
|
61
|
+
position: relative;
|
|
62
|
+
user-select: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.sidebar-nav a:hover { background: var(--bg-hover); color: var(--text); }
|
|
66
|
+
.sidebar-nav a.active { background: var(--bg-active); color: var(--accent); }
|
|
67
|
+
|
|
68
|
+
.sidebar-nav a.active::before {
|
|
69
|
+
content: '';
|
|
70
|
+
position: absolute;
|
|
71
|
+
left: 0;
|
|
72
|
+
top: 0;
|
|
73
|
+
bottom: 0;
|
|
74
|
+
width: 2px;
|
|
75
|
+
background: var(--accent);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ── Sidebar footer (update banner) ────────────── */
|
|
79
|
+
|
|
80
|
+
.sidebar-footer {
|
|
81
|
+
margin-top: auto;
|
|
82
|
+
font-size: 11px;
|
|
83
|
+
color: var(--text-dim);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.sidebar-footer:empty { display: none; }
|
|
87
|
+
|
|
88
|
+
.sidebar-footer:not(:empty) {
|
|
89
|
+
padding: 12px 16px;
|
|
90
|
+
border-top: 1px solid var(--border);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.sidebar-update-btn {
|
|
94
|
+
width: 100%;
|
|
95
|
+
font-size: 11px;
|
|
96
|
+
padding: 5px 10px;
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
border: 1px solid rgba(227, 179, 65, 0.2);
|
|
99
|
+
color: #e3b341;
|
|
100
|
+
background: rgba(227, 179, 65, 0.08);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
font-family: inherit;
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
text-align: center;
|
|
105
|
+
transition: background 0.15s, border-color 0.15s;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.sidebar-update-btn:hover { background: rgba(227, 179, 65, 0.14); border-color: rgba(227, 179, 65, 0.35); }
|
|
109
|
+
.sidebar-update-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
110
|
+
|
|
111
|
+
/* ── Statusbar ─────────────────────────────────── */
|
|
112
|
+
|
|
113
|
+
.app-statusbar {
|
|
114
|
+
grid-column: 1 / -1;
|
|
115
|
+
background: var(--bg-sidebar);
|
|
116
|
+
border-top: 1px solid var(--border);
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: space-between;
|
|
120
|
+
padding: 0 12px;
|
|
121
|
+
font-size: 11px;
|
|
122
|
+
color: var(--text-dim);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.app-statusbar a { color: var(--text-muted); text-decoration: none; }
|
|
126
|
+
.app-statusbar a:hover { color: var(--accent); }
|
|
127
|
+
|
|
128
|
+
.statusbar-left, .statusbar-right { display: flex; align-items: center; gap: 16px; margin-left: 2px; }
|
|
129
|
+
|
|
130
|
+
/* ── Responsive ────────────────────────────────── */
|
|
131
|
+
|
|
132
|
+
@media (max-width: 768px) {
|
|
133
|
+
.app-shell { grid-template-columns: 1fr; }
|
|
134
|
+
.app-sidebar { display: none; }
|
|
135
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0a0e14;
|
|
3
|
+
--bg-sidebar: #0d1117;
|
|
4
|
+
--bg-content: #0a0e14;
|
|
5
|
+
--bg-hover: rgba(99, 235, 255, 0.05);
|
|
6
|
+
--bg-active: rgba(99, 235, 255, 0.08);
|
|
7
|
+
--border: rgba(255, 255, 255, 0.06);
|
|
8
|
+
--text: #c9d1d9;
|
|
9
|
+
--text-muted: #6e7681;
|
|
10
|
+
--text-dim: #383d47;
|
|
11
|
+
--accent: #63ebff;
|
|
12
|
+
--accent-dim: rgba(99, 235, 255, 0.4);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
html, body { height: 100%; }
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
background: var(--bg);
|
|
19
|
+
color: var(--text);
|
|
20
|
+
font-family: 'JetBrains Mono', monospace;
|
|
21
|
+
font-size: 13px;
|
|
22
|
+
line-height: 1.6;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Subtle grid texture overlay */
|
|
26
|
+
body::before {
|
|
27
|
+
content: '';
|
|
28
|
+
position: fixed;
|
|
29
|
+
inset: 0;
|
|
30
|
+
background-image:
|
|
31
|
+
linear-gradient(rgba(255, 255, 255, 0.015) 1px, transparent 1px),
|
|
32
|
+
linear-gradient(90deg, rgba(255, 255, 255, 0.015) 1px, transparent 1px);
|
|
33
|
+
background-size: 48px 48px;
|
|
34
|
+
pointer-events: none;
|
|
35
|
+
z-index: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
::placeholder { color: var(--text-dim) !important; opacity: 1 !important; }
|
|
39
|
+
::-webkit-input-placeholder { color: var(--text-dim) !important; }
|
|
40
|
+
::-moz-placeholder { color: var(--text-dim) !important; }
|
|
41
|
+
|
|
42
|
+
::-webkit-scrollbar { width: 6px; }
|
|
43
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
44
|
+
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.08); border-radius: 3px; }
|
|
45
|
+
|
|
46
|
+
/* Google scope picker toggle buttons */
|
|
47
|
+
.scope-btn { background: rgba(255,255,255,0.03); color: var(--text-muted); border: 1px solid var(--border); transition: all 0.15s; }
|
|
48
|
+
.scope-btn:hover { border-color: var(--text-dim); color: var(--text); }
|
|
49
|
+
.scope-btn.active { background: var(--bg-active); color: var(--accent); border-color: var(--accent-dim); }
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="48" height="49" viewBox="0 0 48 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M23.5518 8.05469L31.0293 1.10938L30.4102 10.7275L34.4512 12.5791C34.5736 12.6156 36.1485 16.9461 37.2354 18.3857L47.0996 23.7285C46.0146 27.8051 40.8701 31.1234 39.874 31.4355C39.4971 31.5491 37.3802 31.375 35.9883 31.0947C34.838 30.812 31.4115 30.31 31.0049 31.2158C29.8234 33.8208 30.8798 36.9984 32.4238 39.6348L29.0186 38.6973C28.5812 42.8006 26.6524 45.6721 23.1943 48.3086C20.6642 44.2054 17.5347 42.1502 13.3564 40.71L13.5596 43.5186C9.89126 39.7415 7.26666 36.0583 6.08496 31.5176L2.95508 34.8965C2.31234 30.0298 2.51557 26.3366 4.38574 22.8086L0 23.9678C1.80268 21.0681 3.67334 18.3856 6.46191 16.0586L4.47754 15.2197C7.46189 12.4892 10.7076 10.6598 13.4189 9.24609C14.7191 5.80743 16.5511 3.07864 20.3691 0L23.5518 8.05469ZM16.3701 31.0635C21.1548 35.592 23.9199 39.2755 24.4492 44.0264C26.8608 40.6795 26.9817 37.9163 27.1025 34.0498L28.46 35.9668C28.1942 34.6209 27.9621 33.2042 28.3994 31.5176C26.5003 32.3803 25.0554 34.0963 24.0889 36.5342C22.3152 34.6203 21.1987 32.1213 20.1113 29.8232L20.792 28.8887L17.4844 28.7334L16.3701 31.0635ZM15.1855 32.7793C14.3447 34.4418 13.3204 36.535 13.3203 38.3594C17.3124 39.86 19.5363 41.049 22.0664 43.4268C20.5028 37.6368 17.0173 34.3501 15.1855 32.7793ZM12.9102 11.9385C11.1992 12.9027 10.0194 13.71 8.72168 14.7129L10.9746 15.6475C8.69107 17.0877 6.79372 18.7144 5.34863 20.4639L9.05273 19.4346C6.70381 22.7066 5.23037 25.4694 5.07812 29.2441L7.50879 26.4043C7.80119 29.0407 7.92229 31.4476 11.1387 37.1699L11.2598 37.0781C11.4894 34.5795 13.1154 32.5249 15.0703 28.54L8.48535 28.6035L12.0303 23.0039L8.81348 23.0645C10.3166 20.614 11.5545 19.0432 13.2969 17.3564C12.7411 15.3652 12.7579 14.0216 12.9102 11.9385ZM24.8379 10.8799C20.9424 13.3182 15.4857 17.7336 13.1367 20.9404L16.2344 20.626L12.7334 26.5059H16.3408L18.6953 26.5371L26.457 26.9795L22.4648 29.9707L23.7627 32.3799C25.5365 29.7923 29.0377 28.4145 31.8262 28.4434C34.4216 28.4748 38.6897 29.1903 39.5283 29.2217C40.495 29.0984 42.3294 27.6994 43.168 26.6289C42.3609 25.9933 41.5223 24.8043 41.7832 23.3037L35.4854 20.0166L32.7158 14.3184L24.8379 10.8799ZM34.0117 19.6084L31.4648 18.8936C30.5297 19.0482 29.5968 19.0479 28.8477 18.0186C28.2603 17.2091 26.8171 16.6438 25.377 16.1992L31.4336 16.0469L34.0117 19.6084ZM19.4707 3.35449C15.8628 7.5109 14.72 11.4285 15.3047 15.7637L17.0781 14.1377C16.7858 11.9339 16.9868 9.63498 18.0742 7.19434L19.8447 12.0615L22.0664 10.2393L19.4707 3.35449ZM25.9131 8.80664L28.165 9.83594L28.3682 6.36816L25.9131 8.80664Z" fill="#00EFFF"/>
|
|
3
|
+
</svg>
|
package/lib/public/js/app.js
CHANGED
|
@@ -292,9 +292,10 @@ function App() {
|
|
|
292
292
|
// Still loading onboard status
|
|
293
293
|
if (onboarded === null) {
|
|
294
294
|
return html`
|
|
295
|
-
<div class="
|
|
295
|
+
<div class="min-h-screen flex items-center justify-center" style="position: relative; z-index: 1">
|
|
296
296
|
<svg
|
|
297
|
-
class="animate-spin h-6 w-6
|
|
297
|
+
class="animate-spin h-6 w-6"
|
|
298
|
+
style="color: var(--text-muted)"
|
|
298
299
|
viewBox="0 0 24 24"
|
|
299
300
|
fill="none"
|
|
300
301
|
>
|
|
@@ -319,71 +320,69 @@ function App() {
|
|
|
319
320
|
|
|
320
321
|
if (!onboarded) {
|
|
321
322
|
return html`
|
|
322
|
-
|
|
323
|
+
<div class="min-h-screen flex justify-center pt-12 pb-8 px-4" style="position: relative; z-index: 1">
|
|
324
|
+
<${Welcome} onComplete=${() => setOnboarded(true)} />
|
|
325
|
+
</div>
|
|
323
326
|
<${ToastContainer} />
|
|
324
327
|
`;
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
const kNavItems = [
|
|
331
|
+
{ id: "general", label: "General" },
|
|
332
|
+
{ id: "models", label: "Models" },
|
|
333
|
+
{ id: "envars", label: "Envars" },
|
|
334
|
+
];
|
|
335
|
+
|
|
327
336
|
return html`
|
|
328
|
-
<div class="
|
|
329
|
-
<div class="
|
|
330
|
-
<div class="
|
|
331
|
-
<
|
|
332
|
-
<
|
|
333
|
-
<h1 class="text-2xl font-semibold">OpenClaw Setup${acVersion ? html`${" "}<span class="text-base font-normal text-gray-600">v${acVersion}</span>` : ""}</h1>
|
|
334
|
-
<p class="text-gray-500 text-sm">This should be easy, right?</p>
|
|
335
|
-
</div>
|
|
337
|
+
<div class="app-shell">
|
|
338
|
+
<div class="app-sidebar">
|
|
339
|
+
<div class="sidebar-brand">
|
|
340
|
+
<img src="./img/logo.svg" alt="" width="20" height="20" />
|
|
341
|
+
<span><span style="color: var(--accent)">alpha</span>claw</span>
|
|
336
342
|
</div>
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
<button
|
|
345
|
-
onclick=${handleAcUpdate}
|
|
346
|
-
disabled=${acUpdating}
|
|
347
|
-
class="text-xs font-medium px-3 py-1 rounded-lg bg-yellow-500/20 text-yellow-300 hover:bg-yellow-500/30 transition-colors ${acUpdating ? "opacity-50 cursor-not-allowed" : ""}"
|
|
348
|
-
>
|
|
349
|
-
${acUpdating ? "Updating..." : "Update now"}
|
|
350
|
-
</button>
|
|
351
|
-
<button
|
|
352
|
-
onclick=${() => setAcDismissed(true)}
|
|
353
|
-
class="text-yellow-500/60 hover:text-yellow-400 transition-colors"
|
|
354
|
-
title="Dismiss"
|
|
355
|
-
>
|
|
356
|
-
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
|
|
357
|
-
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"/>
|
|
358
|
-
</svg>
|
|
359
|
-
</button>
|
|
360
|
-
</div>
|
|
361
|
-
</div>
|
|
362
|
-
`}
|
|
363
|
-
|
|
364
|
-
<div class="flex gap-1 border-b border-border">
|
|
365
|
-
${["general", "models", "envars"].map(
|
|
366
|
-
(t) => html`
|
|
367
|
-
<button
|
|
368
|
-
onclick=${() => setTab(t)}
|
|
369
|
-
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors ${tab ===
|
|
370
|
-
t
|
|
371
|
-
? "border-white text-white"
|
|
372
|
-
: "border-transparent text-gray-500 hover:text-gray-300"}"
|
|
343
|
+
<div class="sidebar-label">Setup</div>
|
|
344
|
+
<nav class="sidebar-nav">
|
|
345
|
+
${kNavItems.map(
|
|
346
|
+
(item) => html`
|
|
347
|
+
<a
|
|
348
|
+
class=${tab === item.id ? "active" : ""}
|
|
349
|
+
onclick=${() => setTab(item.id)}
|
|
373
350
|
>
|
|
374
|
-
${
|
|
375
|
-
</
|
|
351
|
+
${item.label}
|
|
352
|
+
</a>
|
|
376
353
|
`,
|
|
377
354
|
)}
|
|
355
|
+
</nav>
|
|
356
|
+
<div class="sidebar-footer">
|
|
357
|
+
${acHasUpdate && acLatest && !acDismissed ? html`
|
|
358
|
+
<button
|
|
359
|
+
onclick=${handleAcUpdate}
|
|
360
|
+
disabled=${acUpdating}
|
|
361
|
+
class="sidebar-update-btn"
|
|
362
|
+
>${acUpdating ? "Updating..." : `Update to v${acLatest}`}</button>
|
|
363
|
+
` : null}
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<div class="app-content">
|
|
368
|
+
<div class="max-w-2xl space-y-4">
|
|
369
|
+
${tab === "general"
|
|
370
|
+
? html`<${GeneralTab} onSwitchTab=${setTab} />`
|
|
371
|
+
: tab === "models"
|
|
372
|
+
? html`<${Models} />`
|
|
373
|
+
: html`<${Envars} />`}
|
|
378
374
|
</div>
|
|
379
375
|
</div>
|
|
380
376
|
|
|
381
|
-
<div class="
|
|
382
|
-
|
|
383
|
-
? html
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
377
|
+
<div class="app-statusbar">
|
|
378
|
+
<div class="statusbar-left">
|
|
379
|
+
${acVersion ? html`<span style="color: var(--text-muted)">v${acVersion}</span>` : null}
|
|
380
|
+
</div>
|
|
381
|
+
<div class="statusbar-right">
|
|
382
|
+
<a href="https://docs.openclaw.ai" target="_blank" rel="noreferrer">docs</a>
|
|
383
|
+
<a href="https://discord.com/invite/clawd" target="_blank" rel="noreferrer">discord</a>
|
|
384
|
+
<a href="https://github.com/openclaw/openclaw" target="_blank" rel="noreferrer">github</a>
|
|
385
|
+
</div>
|
|
387
386
|
</div>
|
|
388
387
|
</div>
|
|
389
388
|
<${ToastContainer} />
|
|
@@ -10,7 +10,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
10
10
|
const [email, setEmail] = useState("");
|
|
11
11
|
const [error, setError] = useState("");
|
|
12
12
|
const [saving, setSaving] = useState(false);
|
|
13
|
-
const [instrType, setInstrType] = useState("
|
|
13
|
+
const [instrType, setInstrType] = useState("workspace");
|
|
14
14
|
const [redirectUriCopied, setRedirectUriCopied] = useState(false);
|
|
15
15
|
const fileRef = useRef(null);
|
|
16
16
|
|
|
@@ -63,7 +63,14 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
const btnCls = (type) =>
|
|
66
|
-
`
|
|
66
|
+
`flex-1 text-center border-0 cursor-pointer transition-colors` +
|
|
67
|
+
` ${instrType === type ? "" : "hover:text-white"}`;
|
|
68
|
+
|
|
69
|
+
const btnStyle = (type) =>
|
|
70
|
+
`font-family: inherit; font-size: 11px; letter-spacing: 0.03em; padding: 5px 10px;` +
|
|
71
|
+
(instrType === type
|
|
72
|
+
? ` color: var(--accent); background: var(--bg-active);`
|
|
73
|
+
: ` color: var(--text-muted); background: transparent;`);
|
|
67
74
|
|
|
68
75
|
const renderRedirectUriInstruction = () => html`
|
|
69
76
|
<div class="mt-1 flex items-center gap-2">
|
|
@@ -92,7 +99,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
92
99
|
}}
|
|
93
100
|
>
|
|
94
101
|
<div
|
|
95
|
-
class="bg-surface border border-border rounded-xl p-6 max-w-
|
|
102
|
+
class="bg-surface border border-border rounded-xl p-6 max-w-lg w-full space-y-4"
|
|
96
103
|
>
|
|
97
104
|
<h2 class="text-lg font-semibold">Connect Google Workspace</h2>
|
|
98
105
|
<div class="space-y-3">
|
|
@@ -102,8 +109,9 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
102
109
|
<a
|
|
103
110
|
href="https://console.cloud.google.com/apis/credentials"
|
|
104
111
|
target="_blank"
|
|
105
|
-
class="
|
|
106
|
-
|
|
112
|
+
class="hover:text-white"
|
|
113
|
+
style="color: var(--accent)"
|
|
114
|
+
>Create one →</a
|
|
107
115
|
>
|
|
108
116
|
</p>
|
|
109
117
|
<details
|
|
@@ -112,139 +120,135 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
112
120
|
<summary class="cursor-pointer font-medium hover:text-gray-200">
|
|
113
121
|
Step-by-step instructions
|
|
114
122
|
</summary>
|
|
115
|
-
<div
|
|
123
|
+
<div
|
|
124
|
+
class="mt-2 mb-2 flex overflow-hidden"
|
|
125
|
+
style="border: 1px solid var(--border); border-radius: 6px; background: rgba(255,255,255,0.02)"
|
|
126
|
+
>
|
|
116
127
|
<button
|
|
117
128
|
onclick=${() => setInstrType("workspace")}
|
|
118
129
|
class=${btnCls("workspace")}
|
|
130
|
+
style=${btnStyle("workspace")}
|
|
119
131
|
>
|
|
120
132
|
Google Workspace
|
|
121
133
|
</button>
|
|
122
134
|
<button
|
|
123
135
|
onclick=${() => setInstrType("personal")}
|
|
124
136
|
class=${btnCls("personal")}
|
|
137
|
+
style=${btnStyle("personal")}
|
|
125
138
|
>
|
|
126
139
|
Personal Gmail
|
|
127
140
|
</button>
|
|
128
141
|
</div>
|
|
129
142
|
${instrType === "personal"
|
|
130
143
|
? html`
|
|
131
|
-
<div>
|
|
132
|
-
<ol class="list-decimal list-inside space-y-
|
|
144
|
+
<div style="line-height: 1.7">
|
|
145
|
+
<ol class="list-decimal list-inside space-y-2.5 ml-1">
|
|
133
146
|
<li>
|
|
134
|
-
${" "}
|
|
135
|
-
<a
|
|
147
|
+
${" "}<a
|
|
136
148
|
href="https://console.cloud.google.com/projectcreate"
|
|
137
149
|
target="_blank"
|
|
138
|
-
class="
|
|
150
|
+
class="hover:text-white"
|
|
151
|
+
style="color: var(--accent)"
|
|
139
152
|
>Create a Google Cloud project</a
|
|
140
|
-
|
|
141
|
-
(or use existing)
|
|
153
|
+
>${" "}(or use existing)
|
|
142
154
|
</li>
|
|
143
155
|
<li>
|
|
144
|
-
Go to${" "}
|
|
145
|
-
<a
|
|
156
|
+
Go to${" "}<a
|
|
146
157
|
href="https://console.cloud.google.com/auth/audience"
|
|
147
158
|
target="_blank"
|
|
148
|
-
class="
|
|
159
|
+
class="hover:text-white"
|
|
160
|
+
style="color: var(--accent)"
|
|
149
161
|
>OAuth consent screen</a
|
|
150
|
-
>
|
|
151
|
-
→ set to <strong>External</strong>
|
|
162
|
+
>${" "}→ set to <strong>External</strong>
|
|
152
163
|
</li>
|
|
153
164
|
<li>
|
|
154
|
-
Under${" "}
|
|
155
|
-
<a
|
|
165
|
+
Under${" "}<a
|
|
156
166
|
href="https://console.cloud.google.com/auth/audience"
|
|
157
167
|
target="_blank"
|
|
158
|
-
class="
|
|
168
|
+
class="hover:text-white"
|
|
169
|
+
style="color: var(--accent)"
|
|
159
170
|
>Test users</a
|
|
160
171
|
>, <strong>add your own email</strong>
|
|
161
172
|
</li>
|
|
162
173
|
<li>
|
|
163
|
-
${" "}
|
|
164
|
-
<a
|
|
174
|
+
${" "}<a
|
|
165
175
|
href="https://console.cloud.google.com/apis/library"
|
|
166
176
|
target="_blank"
|
|
167
|
-
class="
|
|
177
|
+
class="hover:text-white"
|
|
178
|
+
style="color: var(--accent)"
|
|
168
179
|
>Enable APIs</a
|
|
169
|
-
|
|
170
|
-
for the services you selected below
|
|
180
|
+
>${" "}for the services you selected below
|
|
171
181
|
</li>
|
|
172
182
|
<li>
|
|
173
|
-
Go to${" "}
|
|
174
|
-
<a
|
|
183
|
+
Go to${" "}<a
|
|
175
184
|
href="https://console.cloud.google.com/apis/credentials"
|
|
176
185
|
target="_blank"
|
|
177
|
-
class="
|
|
186
|
+
class="hover:text-white"
|
|
187
|
+
style="color: var(--accent)"
|
|
178
188
|
>Credentials</a
|
|
179
|
-
|
|
180
|
-
→ Create OAuth 2.0 Client ID (Web application)
|
|
189
|
+
>${" "}→ Create OAuth 2.0 Client ID (Web application)
|
|
181
190
|
</li>
|
|
182
191
|
<li>
|
|
183
|
-
Add redirect URI
|
|
184
|
-
${renderRedirectUriInstruction()}
|
|
192
|
+
Add redirect URI:${renderRedirectUriInstruction()}
|
|
185
193
|
</li>
|
|
186
194
|
<li>
|
|
187
195
|
Copy Client ID + Secret (or download credentials JSON)
|
|
188
196
|
</li>
|
|
189
197
|
</ol>
|
|
190
|
-
<p class="mt-
|
|
198
|
+
<p class="mt-3 text-yellow-500/80">
|
|
191
199
|
⚠️ App will be in "Testing" mode. Only emails added as
|
|
192
200
|
Test Users can sign in (up to 100).
|
|
193
201
|
</p>
|
|
194
202
|
</div>
|
|
195
203
|
`
|
|
196
204
|
: html`
|
|
197
|
-
<div>
|
|
198
|
-
<ol class="list-decimal list-inside space-y-
|
|
205
|
+
<div style="line-height: 1.7">
|
|
206
|
+
<ol class="list-decimal list-inside space-y-2.5 ml-1">
|
|
199
207
|
<li>
|
|
200
|
-
${" "}
|
|
201
|
-
<a
|
|
208
|
+
${" "}<a
|
|
202
209
|
href="https://console.cloud.google.com/projectcreate"
|
|
203
210
|
target="_blank"
|
|
204
|
-
class="
|
|
211
|
+
class="hover:text-white"
|
|
212
|
+
style="color: var(--accent)"
|
|
205
213
|
>Create a Google Cloud project</a
|
|
206
|
-
|
|
207
|
-
(or use existing)
|
|
214
|
+
>${" "}(or use existing)
|
|
208
215
|
</li>
|
|
209
216
|
<li>
|
|
210
|
-
Go to${" "}
|
|
211
|
-
<a
|
|
217
|
+
Go to${" "}<a
|
|
212
218
|
href="https://console.cloud.google.com/auth/audience"
|
|
213
219
|
target="_blank"
|
|
214
|
-
class="
|
|
220
|
+
class="hover:text-white"
|
|
221
|
+
style="color: var(--accent)"
|
|
215
222
|
>OAuth consent screen</a
|
|
216
|
-
>
|
|
217
|
-
|
|
223
|
+
>${" "}→ set to <strong>Internal</strong> (Workspace
|
|
224
|
+
only)
|
|
218
225
|
</li>
|
|
219
226
|
<li>
|
|
220
|
-
${" "}
|
|
221
|
-
<a
|
|
227
|
+
${" "}<a
|
|
222
228
|
href="https://console.cloud.google.com/apis/library"
|
|
223
229
|
target="_blank"
|
|
224
|
-
class="
|
|
230
|
+
class="hover:text-white"
|
|
231
|
+
style="color: var(--accent)"
|
|
225
232
|
>Enable APIs</a
|
|
226
|
-
|
|
227
|
-
for the services you selected below
|
|
233
|
+
>${" "}for the services you selected below
|
|
228
234
|
</li>
|
|
229
235
|
<li>
|
|
230
|
-
Go to${" "}
|
|
231
|
-
<a
|
|
236
|
+
Go to${" "}<a
|
|
232
237
|
href="https://console.cloud.google.com/apis/credentials"
|
|
233
238
|
target="_blank"
|
|
234
|
-
class="
|
|
239
|
+
class="hover:text-white"
|
|
240
|
+
style="color: var(--accent)"
|
|
235
241
|
>Credentials</a
|
|
236
|
-
|
|
237
|
-
→ Create OAuth 2.0 Client ID (Web application)
|
|
242
|
+
>${" "}→ Create OAuth 2.0 Client ID (Web application)
|
|
238
243
|
</li>
|
|
239
244
|
<li>
|
|
240
|
-
Add redirect URI
|
|
241
|
-
${renderRedirectUriInstruction()}
|
|
245
|
+
Add redirect URI:${renderRedirectUriInstruction()}
|
|
242
246
|
</li>
|
|
243
247
|
<li>
|
|
244
248
|
Copy Client ID + Secret (or download credentials JSON)
|
|
245
249
|
</li>
|
|
246
250
|
</ol>
|
|
247
|
-
<p class="mt-
|
|
251
|
+
<p class="mt-3 text-green-500/80">
|
|
248
252
|
✓ Internal apps skip test users and verification. Only
|
|
249
253
|
users in your Workspace org can authorize this Google app.
|
|
250
254
|
</p>
|
|
@@ -15,15 +15,15 @@ const kGroupLabels = {
|
|
|
15
15
|
const kGroupOrder = ["github", "channels", "tools", "custom"];
|
|
16
16
|
|
|
17
17
|
const kHintByKey = {
|
|
18
|
-
ANTHROPIC_API_KEY: html`
|
|
19
|
-
ANTHROPIC_TOKEN: html`
|
|
20
|
-
OPENAI_API_KEY: html`
|
|
21
|
-
GEMINI_API_KEY: html`
|
|
22
|
-
GITHUB_TOKEN: html`
|
|
23
|
-
GITHUB_WORKSPACE_REPO: html`
|
|
24
|
-
TELEGRAM_BOT_TOKEN: html`
|
|
25
|
-
DISCORD_BOT_TOKEN: html`
|
|
26
|
-
BRAVE_API_KEY: html`
|
|
18
|
+
ANTHROPIC_API_KEY: html`from <a href="https://console.anthropic.com" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">console.anthropic.com</a>`,
|
|
19
|
+
ANTHROPIC_TOKEN: html`from <code class="text-xs bg-black/30 px-1 rounded">claude setup-token</code>`,
|
|
20
|
+
OPENAI_API_KEY: html`from <a href="https://platform.openai.com" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">platform.openai.com</a>`,
|
|
21
|
+
GEMINI_API_KEY: html`from <a href="https://aistudio.google.com" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">aistudio.google.com</a>`,
|
|
22
|
+
GITHUB_TOKEN: html`classic PAT · <code class="text-xs bg-black/30 px-1 rounded">repo</code> scope · <a href="https://github.com/settings/tokens" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">github settings</a>`,
|
|
23
|
+
GITHUB_WORKSPACE_REPO: html`use <code class="text-xs bg-black/30 px-1 rounded">owner/repo</code> or <code class="text-xs bg-black/30 px-1 rounded">https://github.com/owner/repo</code>`,
|
|
24
|
+
TELEGRAM_BOT_TOKEN: html`from <a href="https://t.me/BotFather" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">@BotFather</a> · <a href="https://docs.openclaw.ai/channels/telegram" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">full guide</a>`,
|
|
25
|
+
DISCORD_BOT_TOKEN: html`from <a href="https://discord.com/developers/applications" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">developer portal</a> · <a href="https://docs.openclaw.ai/channels/discord" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">full guide</a>`,
|
|
26
|
+
BRAVE_API_KEY: html`from <a href="https://brave.com/search/api/" target="_blank" class="hover:underline" style="color: rgba(99, 235, 255, 0.6)">brave.com/search/api</a> — free tier available`,
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const getHintContent = (envVar) => kHintByKey[envVar.key] || envVar.hint || "";
|
|
@@ -31,20 +31,19 @@ const getHintContent = (envVar) => kHintByKey[envVar.key] || envVar.hint || "";
|
|
|
31
31
|
const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
32
32
|
const [visible, setVisible] = useState(false);
|
|
33
33
|
const isSecret = !!envVar.value;
|
|
34
|
+
const hint = getHintContent(envVar);
|
|
34
35
|
|
|
35
36
|
return html`
|
|
36
|
-
<div class="flex items-
|
|
37
|
+
<div class="flex items-start gap-4 px-4 py-3">
|
|
38
|
+
<div class="shrink-0 flex items-center gap-2 pt-1.5" style="width: 200px">
|
|
39
|
+
<span
|
|
40
|
+
class="inline-block w-1.5 h-1.5 rounded-full shrink-0 ${envVar.value
|
|
41
|
+
? "bg-green-500"
|
|
42
|
+
: "bg-gray-600"}"
|
|
43
|
+
/>
|
|
44
|
+
<code class="text-xs truncate" style="color: var(--text-muted)">${envVar.key}</code>
|
|
45
|
+
</div>
|
|
37
46
|
<div class="flex-1 min-w-0">
|
|
38
|
-
<div class="flex items-center gap-2 mb-1">
|
|
39
|
-
<span
|
|
40
|
-
class="inline-block w-2 h-2 rounded-full ${envVar.value
|
|
41
|
-
? "bg-green-500"
|
|
42
|
-
: "bg-gray-600"}"
|
|
43
|
-
/>
|
|
44
|
-
<label class="text-xs font-medium text-gray-400">
|
|
45
|
-
${envVar.label || envVar.key}
|
|
46
|
-
</label>
|
|
47
|
-
</div>
|
|
48
47
|
<div class="flex items-center gap-1">
|
|
49
48
|
<input
|
|
50
49
|
type=${isSecret && !visible ? "password" : "text"}
|
|
@@ -73,10 +72,8 @@ const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
|
73
72
|
</button>`
|
|
74
73
|
: null}
|
|
75
74
|
</div>
|
|
76
|
-
${
|
|
77
|
-
? html`<p class="text-xs text-gray-600 mt-1"
|
|
78
|
-
${getHintContent(envVar)}
|
|
79
|
-
</p>`
|
|
75
|
+
${hint
|
|
76
|
+
? html`<p class="text-xs text-gray-600 mt-1">${hint}</p>`
|
|
80
77
|
: null}
|
|
81
78
|
</div>
|
|
82
79
|
</div>
|
|
@@ -267,57 +264,59 @@ export const Envars = () => {
|
|
|
267
264
|
.filter((g) => grouped[g]?.length)
|
|
268
265
|
.map(
|
|
269
266
|
(g) => html`
|
|
270
|
-
<div
|
|
271
|
-
class="
|
|
272
|
-
>
|
|
273
|
-
<h3 class="text-sm font-medium text-gray-400">
|
|
267
|
+
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
268
|
+
<h3 class="text-xs font-semibold px-4 pt-3 pb-2" style="color: var(--text-dim); letter-spacing: 0.05em">
|
|
274
269
|
${kGroupLabels[g] || g}
|
|
275
270
|
</h3>
|
|
276
|
-
|
|
277
|
-
(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
271
|
+
<div class="divide-y divide-border">
|
|
272
|
+
${grouped[g].map(
|
|
273
|
+
(v) =>
|
|
274
|
+
html`<${EnvRow}
|
|
275
|
+
envVar=${v}
|
|
276
|
+
onChange=${handleChange}
|
|
277
|
+
onDelete=${handleDelete}
|
|
278
|
+
disabled=${saving}
|
|
279
|
+
/>`,
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
285
282
|
</div>
|
|
286
283
|
`,
|
|
287
284
|
)}
|
|
288
285
|
|
|
289
|
-
<div class="bg-surface border border-border rounded-xl
|
|
290
|
-
<div class="flex items-center justify-between">
|
|
291
|
-
<h3 class="text-
|
|
292
|
-
<span class="text-xs text-
|
|
293
|
-
>Paste KEY=VALUE or multiple lines</span
|
|
294
|
-
>
|
|
286
|
+
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
287
|
+
<div class="flex items-center justify-between px-4 pt-3 pb-2">
|
|
288
|
+
<h3 class="text-xs font-semibold" style="color: var(--text-dim); letter-spacing: 0.05em">Add Variable</h3>
|
|
289
|
+
<span class="text-xs" style="color: var(--text-dim)">Paste KEY=VALUE or multiple lines</span>
|
|
295
290
|
</div>
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
291
|
+
<div class="flex items-start gap-4 px-4 py-3 border-t border-border">
|
|
292
|
+
<div class="shrink-0" style="width: 200px">
|
|
293
|
+
<input
|
|
294
|
+
type="text"
|
|
295
|
+
value=${newKey}
|
|
296
|
+
placeholder="KEY"
|
|
297
|
+
onInput=${(e) => handleKeyInput(e.target.value)}
|
|
298
|
+
onPaste=${(e) => handlePaste(e, "key")}
|
|
299
|
+
onKeyDown=${(e) => e.key === "Enter" && handleAddVar()}
|
|
300
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono uppercase"
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="flex-1 flex gap-2">
|
|
304
|
+
<input
|
|
305
|
+
type="text"
|
|
306
|
+
value=${newVal}
|
|
307
|
+
placeholder="value"
|
|
308
|
+
onInput=${(e) => handleValInput(e.target.value)}
|
|
309
|
+
onPaste=${(e) => handlePaste(e, "val")}
|
|
310
|
+
onKeyDown=${(e) => e.key === "Enter" && handleAddVar()}
|
|
311
|
+
class="flex-1 bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
312
|
+
/>
|
|
313
|
+
<button
|
|
314
|
+
onclick=${handleAddVar}
|
|
315
|
+
class="text-xs px-3 py-1.5 rounded-lg border border-border text-gray-400 hover:text-gray-200 hover:border-gray-500 shrink-0"
|
|
316
|
+
>
|
|
317
|
+
+ Add
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
321
320
|
</div>
|
|
322
321
|
</div>
|
|
323
322
|
|
|
@@ -175,7 +175,7 @@ export function Google({ gatewayStatus }) {
|
|
|
175
175
|
<button
|
|
176
176
|
onclick=${() => startAuth(email)}
|
|
177
177
|
disabled=${isAuthed && !scopesChanged}
|
|
178
|
-
class="text-
|
|
178
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg ${isAuthed &&
|
|
179
179
|
!scopesChanged
|
|
180
180
|
? "bg-gray-600 text-gray-400 cursor-not-allowed"
|
|
181
181
|
: "bg-white text-black hover:opacity-85"}"
|
|
@@ -185,7 +185,7 @@ export function Google({ gatewayStatus }) {
|
|
|
185
185
|
<button
|
|
186
186
|
type="button"
|
|
187
187
|
onclick=${() => setModalOpen(true)}
|
|
188
|
-
class="text-xs font-medium px-3 py-
|
|
188
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
|
|
189
189
|
>
|
|
190
190
|
Edit credentials
|
|
191
191
|
</button>
|
|
@@ -35,7 +35,7 @@ const kGroups = [
|
|
|
35
35
|
hint: html`Create a classic PAT at${" "}<a
|
|
36
36
|
href="https://github.com/settings/tokens"
|
|
37
37
|
target="_blank"
|
|
38
|
-
class="
|
|
38
|
+
class="hover:underline" style="color: var(--accent)"
|
|
39
39
|
>github.com/settings/tokens</a
|
|
40
40
|
>${" "}with${" "}<code class="text-xs bg-black/30 px-1 rounded">repo</code>${" "}scope`,
|
|
41
41
|
placeholder: "ghp_...",
|
|
@@ -61,12 +61,12 @@ const kGroups = [
|
|
|
61
61
|
hint: html`From${" "}<a
|
|
62
62
|
href="https://t.me/BotFather"
|
|
63
63
|
target="_blank"
|
|
64
|
-
class="
|
|
64
|
+
class="hover:underline" style="color: var(--accent)"
|
|
65
65
|
>@BotFather</a
|
|
66
66
|
>${" "}·${" "}<a
|
|
67
67
|
href="https://docs.openclaw.ai/channels/telegram"
|
|
68
68
|
target="_blank"
|
|
69
|
-
class="
|
|
69
|
+
class="hover:underline" style="color: var(--accent)"
|
|
70
70
|
>full guide</a
|
|
71
71
|
>`,
|
|
72
72
|
placeholder: "123456789:AAH...",
|
|
@@ -77,12 +77,12 @@ const kGroups = [
|
|
|
77
77
|
hint: html`From${" "}<a
|
|
78
78
|
href="https://discord.com/developers/applications"
|
|
79
79
|
target="_blank"
|
|
80
|
-
class="
|
|
80
|
+
class="hover:underline" style="color: var(--accent)"
|
|
81
81
|
>Developer Portal</a
|
|
82
82
|
>${" "}·${" "}<a
|
|
83
83
|
href="https://docs.openclaw.ai/channels/discord"
|
|
84
84
|
target="_blank"
|
|
85
|
-
class="
|
|
85
|
+
class="hover:underline" style="color: var(--accent)"
|
|
86
86
|
>full guide</a
|
|
87
87
|
>`,
|
|
88
88
|
placeholder: "MTQ3...",
|
|
@@ -101,7 +101,7 @@ const kGroups = [
|
|
|
101
101
|
hint: html`From${" "}<a
|
|
102
102
|
href="https://brave.com/search/api/"
|
|
103
103
|
target="_blank"
|
|
104
|
-
class="
|
|
104
|
+
class="hover:underline" style="color: var(--accent)"
|
|
105
105
|
>brave.com/search/api</a
|
|
106
106
|
>${" "}-${" "}free tier available`,
|
|
107
107
|
placeholder: "BSA...",
|
|
@@ -309,7 +309,8 @@ export const Welcome = ({ onComplete }) => {
|
|
|
309
309
|
if (loading) {
|
|
310
310
|
return html`
|
|
311
311
|
<div
|
|
312
|
-
class="fixed inset-0
|
|
312
|
+
class="fixed inset-0 flex items-center justify-center z-50"
|
|
313
|
+
style="background: var(--bg)"
|
|
313
314
|
>
|
|
314
315
|
<div class="flex flex-col items-center gap-4">
|
|
315
316
|
<svg
|
|
@@ -332,7 +333,7 @@ export const Welcome = ({ onComplete }) => {
|
|
|
332
333
|
/>
|
|
333
334
|
</svg>
|
|
334
335
|
<h2 class="text-lg font-semibold text-white">
|
|
335
|
-
Initializing
|
|
336
|
+
Initializing <span style="color: var(--accent)">alpha</span>claw
|
|
336
337
|
</h2>
|
|
337
338
|
<p class="text-sm text-gray-500">This could take 10–15 seconds</p>
|
|
338
339
|
</div>
|
|
@@ -343,10 +344,10 @@ export const Welcome = ({ onComplete }) => {
|
|
|
343
344
|
return html`
|
|
344
345
|
<div class="max-w-lg w-full space-y-4">
|
|
345
346
|
<div class="flex items-center gap-3">
|
|
346
|
-
<div class="text-4xl"
|
|
347
|
+
<div class="text-4xl">🐾</div>
|
|
347
348
|
<div>
|
|
348
|
-
<h1 class="text-2xl font-semibold">Welcome to
|
|
349
|
-
<p
|
|
349
|
+
<h1 class="text-2xl font-semibold">Welcome to <span style="color: var(--accent)">alpha</span>claw</h1>
|
|
350
|
+
<p style="color: var(--text-muted)" class="text-sm">Let's get your agent running</p>
|
|
350
351
|
</div>
|
|
351
352
|
</div>
|
|
352
353
|
|
package/lib/public/login.html
CHANGED
|
@@ -1,90 +1,135 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>alphaclaw</title>
|
|
7
|
+
<link
|
|
8
|
+
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap"
|
|
9
|
+
rel="stylesheet"
|
|
10
|
+
/>
|
|
11
|
+
<link rel="stylesheet" href="./css/theme.css" />
|
|
12
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
13
|
+
<script>
|
|
14
|
+
tailwind.config = {
|
|
15
|
+
theme: {
|
|
16
|
+
extend: {
|
|
17
|
+
colors: {
|
|
18
|
+
surface: "var(--bg-sidebar)",
|
|
19
|
+
border: "var(--border)",
|
|
20
|
+
},
|
|
21
|
+
fontFamily: {
|
|
22
|
+
mono: ["'JetBrains Mono'", "monospace"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
</script>
|
|
28
|
+
</head>
|
|
29
|
+
<body class="min-h-screen flex items-center justify-center p-4">
|
|
30
|
+
<div class="max-w-sm w-full relative z-10">
|
|
31
|
+
<div class="text-center mb-6">
|
|
32
|
+
<img
|
|
33
|
+
src="./img/logo.svg"
|
|
34
|
+
alt="alphaclaw"
|
|
35
|
+
class="mx-auto mb-4"
|
|
36
|
+
width="48"
|
|
37
|
+
height="49"
|
|
38
|
+
/>
|
|
39
|
+
<h1 class="text-2xl font-semibold mb-2">
|
|
40
|
+
<span style="color: var(--accent)">alpha</span>claw
|
|
41
|
+
</h1>
|
|
42
|
+
<p style="color: var(--text-muted)" class="text-xs mb-4">
|
|
43
|
+
OpenClaw made easier
|
|
44
|
+
</p>
|
|
28
45
|
</div>
|
|
46
|
+
<form
|
|
47
|
+
id="login-form"
|
|
48
|
+
class="bg-surface border border-border rounded-xl p-4 space-y-3"
|
|
49
|
+
>
|
|
50
|
+
<input
|
|
51
|
+
id="password"
|
|
52
|
+
type="password"
|
|
53
|
+
placeholder="Password"
|
|
54
|
+
autofocus
|
|
55
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm outline-none focus:border-gray-500"
|
|
56
|
+
style="color: var(--text)"
|
|
57
|
+
/>
|
|
58
|
+
<div id="error" class="text-red-400 text-xs hidden"></div>
|
|
59
|
+
<button
|
|
60
|
+
id="submit-btn"
|
|
61
|
+
type="submit"
|
|
62
|
+
disabled
|
|
63
|
+
class="w-full text-sm font-medium px-4 py-2 rounded-lg bg-gray-800 text-gray-500 cursor-not-allowed"
|
|
64
|
+
>
|
|
65
|
+
Continue
|
|
66
|
+
</button>
|
|
67
|
+
</form>
|
|
29
68
|
</div>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const submitButtonEl = formEl.querySelector('button[type="submit"]');
|
|
69
|
+
<script>
|
|
70
|
+
const formEl = document.getElementById("login-form");
|
|
71
|
+
const passwordEl = document.getElementById("password");
|
|
72
|
+
const submitButtonEl = document.getElementById("submit-btn");
|
|
73
|
+
|
|
74
|
+
const kEnabledClasses = ["bg-white", "text-black", "hover:opacity-85"];
|
|
75
|
+
const kDisabledClasses = ["bg-gray-800", "text-gray-500", "cursor-not-allowed"];
|
|
76
|
+
|
|
77
|
+
const syncButtonState = () => {
|
|
78
|
+
const hasValue = passwordEl.value.length > 0;
|
|
79
|
+
submitButtonEl.disabled = !hasValue;
|
|
80
|
+
kEnabledClasses.forEach((c) => submitButtonEl.classList.toggle(c, hasValue));
|
|
81
|
+
kDisabledClasses.forEach((c) => submitButtonEl.classList.toggle(c, !hasValue));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
passwordEl.addEventListener("input", syncButtonState);
|
|
47
85
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
const setPendingState = (isPending) => {
|
|
87
|
+
submitButtonEl.disabled = isPending;
|
|
88
|
+
submitButtonEl.classList.toggle("opacity-70", isPending);
|
|
89
|
+
submitButtonEl.classList.toggle("cursor-not-allowed", isPending);
|
|
90
|
+
};
|
|
53
91
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} else {
|
|
70
|
-
if (res.status === 429) {
|
|
71
|
-
const retryAfterFromHeader = Number.parseInt(res.headers.get('retry-after') || '0', 10);
|
|
72
|
-
const retryAfterSec = Number.isFinite(data.retryAfterSec) && data.retryAfterSec > 0
|
|
73
|
-
? data.retryAfterSec
|
|
74
|
-
: (Number.isFinite(retryAfterFromHeader) && retryAfterFromHeader > 0 ? retryAfterFromHeader : 30);
|
|
75
|
-
errorEl.textContent = `Too many attempts. Try again in ${retryAfterSec}s.`;
|
|
92
|
+
formEl.addEventListener("submit", async (e) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
const password = document.getElementById("password").value;
|
|
95
|
+
const errorEl = document.getElementById("error");
|
|
96
|
+
errorEl.classList.add("hidden");
|
|
97
|
+
setPendingState(true);
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch("/api/auth/login", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify({ password }),
|
|
103
|
+
});
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
if (data.ok) {
|
|
106
|
+
window.location.href = "/";
|
|
76
107
|
} else {
|
|
77
|
-
|
|
108
|
+
if (res.status === 429) {
|
|
109
|
+
const retryAfterFromHeader = Number.parseInt(
|
|
110
|
+
res.headers.get("retry-after") || "0",
|
|
111
|
+
10,
|
|
112
|
+
);
|
|
113
|
+
const retryAfterSec =
|
|
114
|
+
Number.isFinite(data.retryAfterSec) && data.retryAfterSec > 0
|
|
115
|
+
? data.retryAfterSec
|
|
116
|
+
: Number.isFinite(retryAfterFromHeader) &&
|
|
117
|
+
retryAfterFromHeader > 0
|
|
118
|
+
? retryAfterFromHeader
|
|
119
|
+
: 30;
|
|
120
|
+
errorEl.textContent = `Too many attempts. Try again in ${retryAfterSec}s.`;
|
|
121
|
+
} else {
|
|
122
|
+
errorEl.textContent = data.error || "Login failed";
|
|
123
|
+
}
|
|
124
|
+
errorEl.classList.remove("hidden");
|
|
78
125
|
}
|
|
79
|
-
|
|
126
|
+
} catch {
|
|
127
|
+
errorEl.textContent = "Connection error";
|
|
128
|
+
errorEl.classList.remove("hidden");
|
|
129
|
+
} finally {
|
|
130
|
+
setPendingState(false);
|
|
80
131
|
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} finally {
|
|
85
|
-
setPendingState(false);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
</script>
|
|
89
|
-
</body>
|
|
132
|
+
});
|
|
133
|
+
</script>
|
|
134
|
+
</body>
|
|
90
135
|
</html>
|
package/lib/public/setup.html
CHANGED
|
@@ -3,31 +3,29 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>alphaclaw</title>
|
|
7
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
8
|
+
<link rel="stylesheet" href="./css/theme.css">
|
|
9
|
+
<link rel="stylesheet" href="./css/shell.css">
|
|
7
10
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
11
|
<script>
|
|
9
12
|
tailwind.config = {
|
|
10
13
|
theme: {
|
|
11
14
|
extend: {
|
|
12
15
|
colors: {
|
|
13
|
-
surface: '
|
|
14
|
-
border: '
|
|
16
|
+
surface: 'var(--bg-sidebar)',
|
|
17
|
+
border: 'var(--border)',
|
|
18
|
+
},
|
|
19
|
+
fontFamily: {
|
|
20
|
+
mono: ["'JetBrains Mono'", 'monospace'],
|
|
15
21
|
}
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
25
|
</script>
|
|
20
|
-
<style>
|
|
21
|
-
::placeholder { color: #444 !important; opacity: 1 !important; }
|
|
22
|
-
::-webkit-input-placeholder { color: #444 !important; }
|
|
23
|
-
::-moz-placeholder { color: #444 !important; }
|
|
24
|
-
.scope-btn { background: #1a1a1a; color: #555; border: 1px solid #333; transition: all 0.15s; }
|
|
25
|
-
.scope-btn:hover { border-color: #555; }
|
|
26
|
-
.scope-btn.active { background: #065f46; color: #6ee7b7; border-color: #059669; }
|
|
27
|
-
</style>
|
|
28
26
|
</head>
|
|
29
|
-
<body
|
|
30
|
-
<div id="app"
|
|
27
|
+
<body>
|
|
28
|
+
<div id="app"></div>
|
|
31
29
|
<script type="module" src="./js/app.js"></script>
|
|
32
30
|
</body>
|
|
33
31
|
</html>
|
|
@@ -6,12 +6,10 @@ const http = require("http");
|
|
|
6
6
|
const {
|
|
7
7
|
kLatestVersionCacheTtlMs,
|
|
8
8
|
kAlphaclawRegistryUrl,
|
|
9
|
-
|
|
9
|
+
kNpmPackageRoot,
|
|
10
|
+
kRootDir,
|
|
10
11
|
} = require("./constants");
|
|
11
12
|
|
|
12
|
-
// kPackageRoot is lib/ — the actual npm package root (with package.json) is one level up
|
|
13
|
-
const kNpmPackageRoot = path.resolve(kPackageRoot, "..");
|
|
14
|
-
|
|
15
13
|
const createAlphaclawVersionService = () => {
|
|
16
14
|
let kUpdateStatusCache = { latestVersion: null, hasUpdate: false, fetchedAt: 0 };
|
|
17
15
|
let kUpdateInProgress = false;
|
|
@@ -184,6 +182,14 @@ const createAlphaclawVersionService = () => {
|
|
|
184
182
|
const previousVersion = readAlphaclawVersion();
|
|
185
183
|
try {
|
|
186
184
|
await installLatestAlphaclaw();
|
|
185
|
+
// Write marker to persistent volume so the update survives container recreation
|
|
186
|
+
const markerPath = path.join(kRootDir, ".alphaclaw-update-pending");
|
|
187
|
+
try {
|
|
188
|
+
fs.writeFileSync(markerPath, JSON.stringify({ from: previousVersion, ts: Date.now() }));
|
|
189
|
+
console.log(`[alphaclaw] Update marker written to ${markerPath}`);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.log(`[alphaclaw] Could not write update marker: ${e.message}`);
|
|
192
|
+
}
|
|
187
193
|
kUpdateStatusCache = { latestVersion: null, hasUpdate: false, fetchedAt: 0 };
|
|
188
194
|
return {
|
|
189
195
|
status: 200,
|
package/lib/server/constants.js
CHANGED
|
@@ -10,6 +10,7 @@ const parsePositiveIntEnv = (value, fallbackValue) => {
|
|
|
10
10
|
const kRootDir = process.env.ALPHACLAW_ROOT_DIR
|
|
11
11
|
|| path.join(os.homedir(), ".alphaclaw");
|
|
12
12
|
const kPackageRoot = path.resolve(__dirname, "..");
|
|
13
|
+
const kNpmPackageRoot = path.resolve(kPackageRoot, "..");
|
|
13
14
|
const kSetupDir = path.join(kPackageRoot, "setup");
|
|
14
15
|
|
|
15
16
|
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
@@ -107,7 +108,7 @@ const kVersionCacheTtlMs = 60 * 1000;
|
|
|
107
108
|
const kLatestVersionCacheTtlMs = 10 * 60 * 1000;
|
|
108
109
|
const kOpenclawRegistryUrl = "https://registry.npmjs.org/openclaw";
|
|
109
110
|
const kAlphaclawRegistryUrl = "https://registry.npmjs.org/@chrysb%2falphaclaw";
|
|
110
|
-
const kAppDir =
|
|
111
|
+
const kAppDir = kNpmPackageRoot;
|
|
111
112
|
|
|
112
113
|
const kSystemVars = new Set([
|
|
113
114
|
"WEBHOOK_TOKEN",
|
|
@@ -236,6 +237,7 @@ const SETUP_API_PREFIXES = [
|
|
|
236
237
|
module.exports = {
|
|
237
238
|
kRootDir,
|
|
238
239
|
kPackageRoot,
|
|
240
|
+
kNpmPackageRoot,
|
|
239
241
|
kSetupDir,
|
|
240
242
|
PORT,
|
|
241
243
|
GATEWAY_PORT,
|