@chrysb/alphaclaw 0.1.20 → 0.1.22
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 +13 -7
- package/lib/public/assets/icons/discord.svg +7 -0
- package/lib/public/assets/icons/telegram.svg +13 -0
- package/lib/public/css/shell.css +51 -3
- package/lib/public/css/theme.css +34 -5
- package/lib/public/js/app.js +57 -12
- package/lib/public/js/components/channels.js +11 -1
- package/lib/public/js/components/envars.js +45 -14
- package/lib/public/js/components/models.js +5 -4
- package/lib/public/js/components/onboarding/pairing-utils.js +12 -0
- package/lib/public/js/components/onboarding/welcome-config.js +18 -1
- package/lib/public/js/components/onboarding/welcome-form-step.js +246 -217
- package/lib/public/js/components/onboarding/welcome-header.js +52 -41
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +173 -0
- package/lib/public/js/components/onboarding/welcome-setup-step.js +108 -36
- package/lib/public/js/components/secret-input.js +2 -0
- package/lib/public/js/components/welcome.js +151 -18
- package/lib/public/js/lib/api.js +24 -1
- package/lib/public/js/lib/model-config.js +29 -3
- package/lib/public/login.html +4 -0
- package/lib/server/routes/auth.js +61 -9
- package/lib/server/routes/google.js +0 -1
- package/lib/server/routes/proxy.js +4 -4
- package/lib/server/routes/system.js +36 -11
- package/lib/server.js +20 -2
- package/lib/setup/core-prompts/AGENTS.md +11 -0
- package/lib/setup/core-prompts/TOOLS.md +22 -4
- package/lib/setup/skills/control-ui/SKILL.md +8 -8
- package/package.json +1 -1
package/bin/alphaclaw.js
CHANGED
|
@@ -106,20 +106,26 @@ try {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// ---------------------------------------------------------------------------
|
|
109
|
-
// 4.
|
|
109
|
+
// 4. Ensure <rootDir>/.env exists (seed from template if missing)
|
|
110
110
|
// ---------------------------------------------------------------------------
|
|
111
111
|
|
|
112
112
|
const envFilePath = path.join(rootDir, ".env");
|
|
113
113
|
const setupDir = path.join(__dirname, "..", "lib", "setup");
|
|
114
|
+
const templatePath = path.join(setupDir, "env.template");
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (fs.existsSync(templatePath)) {
|
|
116
|
+
try {
|
|
117
|
+
if (!fs.existsSync(envFilePath) && fs.existsSync(templatePath)) {
|
|
118
118
|
fs.copyFileSync(templatePath, envFilePath);
|
|
119
|
-
console.log(
|
|
119
|
+
console.log(`[alphaclaw] Created env at ${envFilePath}`);
|
|
120
120
|
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.log(`[alphaclaw] .env setup skipped: ${e.message}`);
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// 6. Load .env into process.env
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
123
129
|
if (fs.existsSync(envFilePath)) {
|
|
124
130
|
const content = fs.readFileSync(envFilePath, "utf8");
|
|
125
131
|
for (const line of content.split("\n")) {
|
|
@@ -135,14 +141,14 @@ if (fs.existsSync(envFilePath)) {
|
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
// ---------------------------------------------------------------------------
|
|
138
|
-
//
|
|
144
|
+
// 7. Set OPENCLAW_HOME globally so all child processes inherit it
|
|
139
145
|
// ---------------------------------------------------------------------------
|
|
140
146
|
|
|
141
147
|
process.env.OPENCLAW_HOME = rootDir;
|
|
142
148
|
process.env.OPENCLAW_CONFIG_PATH = path.join(openclawDir, "openclaw.json");
|
|
143
149
|
|
|
144
150
|
// ---------------------------------------------------------------------------
|
|
145
|
-
//
|
|
151
|
+
// 8. Install gog (Google Workspace CLI) if not present
|
|
146
152
|
// ---------------------------------------------------------------------------
|
|
147
153
|
|
|
148
154
|
process.env.XDG_CONFIG_HOME = openclawDir;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -28.5 256 256" role="img" aria-labelledby="title">
|
|
2
|
+
<title>Discord</title>
|
|
3
|
+
<path
|
|
4
|
+
d="M216.856339 16.5966031C200.285002 8.84328665 182.566144 3.2084988 164.041564 0C161.766523 4.11318106 159.108624 9.64549908 157.276099 14.0464379C137.583995 11.0849896 118.072967 11.0849896 98.7430163 14.0464379C96.9108417 9.64549908 94.1925838 4.11318106 91.8971895 0C73.3526068 3.2084988 55.6133949 8.86399117 39.0420583 16.6376612C5.61752293 67.146514 -3.4433191 116.400813 1.08711069 164.955721C23.2560196 181.510915 44.7403634 191.567697 65.8621325 198.148576C71.0772151 190.971126 75.7283628 183.341335 79.7352139 175.300261C72.104019 172.400575 64.7949724 168.822202 57.8887866 164.667963C59.7209612 163.310589 61.5131304 161.891452 63.2445898 160.431257C105.36741 180.133187 151.134928 180.133187 192.754523 160.431257C194.506336 161.891452 196.298154 163.310589 198.110326 164.667963C191.183787 168.842556 183.854737 172.420929 176.223542 175.320965C180.230393 183.341335 184.861538 190.991831 190.096624 198.16893C211.238746 191.588051 232.743023 181.531619 254.911949 164.955721C260.227747 108.668201 245.831087 59.8662432 216.856339 16.5966031ZM85.4738752 135.09489C72.8290281 135.09489 62.4592217 123.290155 62.4592217 108.914901C62.4592217 94.5396472 72.607595 82.7145587 85.4738752 82.7145587C98.3405064 82.7145587 108.709962 94.5189427 108.488529 108.914901C108.508531 123.290155 98.3405064 135.09489 85.4738752 135.09489ZM170.525237 135.09489C157.88039 135.09489 147.510584 123.290155 147.510584 108.914901C147.510584 94.5396472 157.658606 82.7145587 170.525237 82.7145587C183.391518 82.7145587 193.761324 94.5189427 193.539891 108.914901C193.539891 123.290155 183.391518 135.09489 170.525237 135.09489Z"
|
|
5
|
+
fill="#5865F2"
|
|
6
|
+
/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-labelledby="title">
|
|
2
|
+
<title>Telegram</title>
|
|
3
|
+
<defs>
|
|
4
|
+
<linearGradient id="telegram-gradient" x1="120" y1="240" x2="120" y2="0" gradientUnits="userSpaceOnUse">
|
|
5
|
+
<stop offset="0" stop-color="#1d93d2"/>
|
|
6
|
+
<stop offset="1" stop-color="#38b0e3"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
<circle cx="120" cy="120" r="120" fill="url(#telegram-gradient)"/>
|
|
10
|
+
<path d="M81.229 128.772l14.237 39.406s1.78 3.687 3.686 3.687 30.255-29.492 30.255-29.492l31.525-60.89L81.737 118.6z" fill="#c8daea"/>
|
|
11
|
+
<path d="M100.106 138.878l-2.733 29.046s-1.144 8.9 7.754 0 17.415-15.763 17.415-15.763" fill="#a9c6d8"/>
|
|
12
|
+
<path d="M81.486 130.178L52.2 120.636s-3.5-1.42-2.373-4.64c.232-.664.7-1.229 2.1-2.2 6.489-4.523 120.106-45.36 120.106-45.36s3.208-1.081 5.1-.362a2.766 2.766 0 0 1 1.885 2.055 9.357 9.357 0 0 1 .254 2.585c-.009.752-.1 1.449-.169 2.542-.692 11.165-21.4 94.493-21.4 94.493s-1.239 4.876-5.678 5.043a8.13 8.13 0 0 1-5.925-2.292c-8.711-7.493-38.819-27.727-45.472-32.177a1.27 1.27 0 0 1-.546-.9c-.093-.469.417-1.05.417-1.05s52.426-46.6 53.821-51.492c.108-.379-.3-.566-.848-.4-3.482 1.281-63.844 39.4-70.506 43.607a3.21 3.21 0 0 1-1.48.09z" fill="#fff"/>
|
|
13
|
+
</svg>
|
package/lib/public/css/shell.css
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
/* ── Sidebar ───────────────────────────────────── */
|
|
20
20
|
|
|
21
21
|
.app-sidebar {
|
|
22
|
-
background:
|
|
23
|
-
|
|
22
|
+
background:
|
|
23
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
|
24
|
+
var(--bg-sidebar);
|
|
25
|
+
border-right: 1px solid var(--border-strong);
|
|
24
26
|
overflow-y: auto;
|
|
25
27
|
display: flex;
|
|
26
28
|
flex-direction: column;
|
|
@@ -108,12 +110,58 @@
|
|
|
108
110
|
.sidebar-update-btn:hover { background: rgba(227, 179, 65, 0.14); border-color: rgba(227, 179, 65, 0.35); }
|
|
109
111
|
.sidebar-update-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
110
112
|
|
|
113
|
+
/* ── Brand dropdown menu ───────────────────────── */
|
|
114
|
+
|
|
115
|
+
.brand-menu {
|
|
116
|
+
position: relative;
|
|
117
|
+
margin-left: auto;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.brand-menu-trigger {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
width: 24px;
|
|
125
|
+
height: 24px;
|
|
126
|
+
border: none;
|
|
127
|
+
border-radius: 6px;
|
|
128
|
+
background: transparent;
|
|
129
|
+
color: var(--text-dim);
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
transition: background 0.1s, color 0.1s;
|
|
132
|
+
}
|
|
133
|
+
.brand-menu-trigger:hover { background: var(--bg-hover); color: var(--text-muted); }
|
|
134
|
+
|
|
135
|
+
.brand-dropdown {
|
|
136
|
+
position: absolute;
|
|
137
|
+
top: calc(100% + 4px);
|
|
138
|
+
right: 0;
|
|
139
|
+
min-width: 120px;
|
|
140
|
+
background: var(--bg-sidebar);
|
|
141
|
+
border: 1px solid var(--border-strong);
|
|
142
|
+
border-radius: 8px;
|
|
143
|
+
padding: 4px;
|
|
144
|
+
z-index: 50;
|
|
145
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.brand-dropdown a {
|
|
149
|
+
display: block;
|
|
150
|
+
padding: 6px 10px;
|
|
151
|
+
font-size: 12px;
|
|
152
|
+
color: var(--text-muted);
|
|
153
|
+
text-decoration: none;
|
|
154
|
+
border-radius: 5px;
|
|
155
|
+
transition: background 0.1s, color 0.1s;
|
|
156
|
+
}
|
|
157
|
+
.brand-dropdown a:hover { background: var(--bg-hover); color: var(--text); }
|
|
158
|
+
|
|
111
159
|
/* ── Statusbar ─────────────────────────────────── */
|
|
112
160
|
|
|
113
161
|
.app-statusbar {
|
|
114
162
|
grid-column: 1 / -1;
|
|
115
163
|
background: var(--bg-sidebar);
|
|
116
|
-
border-top: 1px solid var(--border);
|
|
164
|
+
border-top: 1px solid var(--border-strong);
|
|
117
165
|
display: flex;
|
|
118
166
|
align-items: center;
|
|
119
167
|
justify-content: space-between;
|
package/lib/public/css/theme.css
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
--bg: #
|
|
3
|
-
--bg-sidebar: #
|
|
4
|
-
--bg-content: #
|
|
2
|
+
--bg: #0d121b;
|
|
3
|
+
--bg-sidebar: #111826;
|
|
4
|
+
--bg-content: #0f1521;
|
|
5
5
|
--bg-hover: rgba(99, 235, 255, 0.05);
|
|
6
6
|
--bg-active: rgba(99, 235, 255, 0.08);
|
|
7
7
|
--border: rgba(255, 255, 255, 0.06);
|
|
8
|
+
--border-strong: rgba(255, 255, 255, 0.11);
|
|
8
9
|
--text: #c9d1d9;
|
|
9
10
|
--text-muted: #6e7681;
|
|
10
11
|
--text-dim: #383d47;
|
|
11
12
|
--accent: #63ebff;
|
|
12
13
|
--accent-dim: rgba(99, 235, 255, 0.4);
|
|
13
14
|
--accent-link: rgba(99, 235, 255, 0.6);
|
|
15
|
+
--panel-bg-contrast: rgba(255, 255, 255, 0.028);
|
|
16
|
+
--panel-border-contrast: rgba(255, 255, 255, 0.11);
|
|
17
|
+
--field-bg-contrast: rgba(0, 0, 0, 0.3);
|
|
18
|
+
--field-border-contrast: rgba(255, 255, 255, 0.13);
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
html, body { height: 100%; }
|
|
@@ -29,13 +34,37 @@ body::before {
|
|
|
29
34
|
position: fixed;
|
|
30
35
|
inset: 0;
|
|
31
36
|
background-image:
|
|
32
|
-
linear-gradient(rgba(255, 255, 255, 0.
|
|
33
|
-
linear-gradient(90deg, rgba(255, 255, 255, 0.
|
|
37
|
+
linear-gradient(rgba(255, 255, 255, 0.024) 1px, transparent 1px),
|
|
38
|
+
linear-gradient(90deg, rgba(255, 255, 255, 0.024) 1px, transparent 1px);
|
|
34
39
|
background-size: 48px 48px;
|
|
35
40
|
pointer-events: none;
|
|
36
41
|
z-index: 0;
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
/* Unified panel treatment across tabs/pages. */
|
|
45
|
+
.bg-surface {
|
|
46
|
+
background: var(--panel-bg-contrast) !important;
|
|
47
|
+
border-color: var(--panel-border-contrast) !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.border-border {
|
|
51
|
+
border-color: var(--panel-border-contrast) !important;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Universal field contrast treatment (all tabs/pages). */
|
|
55
|
+
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
|
|
56
|
+
select,
|
|
57
|
+
textarea {
|
|
58
|
+
background: var(--field-bg-contrast);
|
|
59
|
+
border-color: var(--field-border-contrast);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus,
|
|
63
|
+
select:focus,
|
|
64
|
+
textarea:focus {
|
|
65
|
+
border-color: rgba(255, 255, 255, 0.28);
|
|
66
|
+
}
|
|
67
|
+
|
|
39
68
|
::placeholder { color: var(--text-dim) !important; opacity: 1 !important; }
|
|
40
69
|
::-webkit-input-placeholder { color: var(--text-dim) !important; }
|
|
41
70
|
::-moz-placeholder { color: var(--text-dim) !important; }
|
package/lib/public/js/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h, render } from "https://esm.sh/preact";
|
|
2
|
-
import { useState, useEffect } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import {
|
|
5
5
|
fetchStatus,
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
approveDevice,
|
|
11
11
|
rejectDevice,
|
|
12
12
|
fetchOnboardStatus,
|
|
13
|
+
fetchAuthStatus,
|
|
14
|
+
logout,
|
|
13
15
|
fetchDashboardUrl,
|
|
14
16
|
updateSyncCron,
|
|
15
17
|
fetchAlphaclawVersion,
|
|
@@ -27,7 +29,6 @@ import { Envars } from "./components/envars.js";
|
|
|
27
29
|
import { ToastContainer, showToast } from "./components/toast.js";
|
|
28
30
|
import { ChevronDownIcon } from "./components/icons.js";
|
|
29
31
|
const html = htm.bind(h);
|
|
30
|
-
const kUiTabStorageKey = "alphaclaw_ui_tab";
|
|
31
32
|
const kUiTabs = ["general", "models", "envars"];
|
|
32
33
|
const kDefaultUiTab = "general";
|
|
33
34
|
|
|
@@ -278,29 +279,42 @@ const GeneralTab = ({ onSwitchTab, isActive }) => {
|
|
|
278
279
|
function App() {
|
|
279
280
|
const [onboarded, setOnboarded] = useState(null);
|
|
280
281
|
const [tab, setTab] = useState(() => {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return kUiTabs.includes(savedTab) ? savedTab : kDefaultUiTab;
|
|
284
|
-
} catch {
|
|
285
|
-
return kDefaultUiTab;
|
|
286
|
-
}
|
|
282
|
+
const hash = window.location.hash.replace("#", "");
|
|
283
|
+
return kUiTabs.includes(hash) ? hash : kDefaultUiTab;
|
|
287
284
|
});
|
|
288
285
|
const [acVersion, setAcVersion] = useState(null);
|
|
289
286
|
const [acLatest, setAcLatest] = useState(null);
|
|
290
287
|
const [acHasUpdate, setAcHasUpdate] = useState(false);
|
|
291
288
|
const [acUpdating, setAcUpdating] = useState(false);
|
|
292
289
|
const [acDismissed, setAcDismissed] = useState(false);
|
|
290
|
+
const [authEnabled, setAuthEnabled] = useState(false);
|
|
291
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
292
|
+
const menuRef = useRef(null);
|
|
293
|
+
|
|
294
|
+
const closeMenu = useCallback((e) => {
|
|
295
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
296
|
+
setMenuOpen(false);
|
|
297
|
+
}
|
|
298
|
+
}, []);
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (menuOpen) {
|
|
302
|
+
document.addEventListener("click", closeMenu, true);
|
|
303
|
+
return () => document.removeEventListener("click", closeMenu, true);
|
|
304
|
+
}
|
|
305
|
+
}, [menuOpen, closeMenu]);
|
|
293
306
|
|
|
294
307
|
useEffect(() => {
|
|
295
308
|
fetchOnboardStatus()
|
|
296
309
|
.then((data) => setOnboarded(data.onboarded))
|
|
297
310
|
.catch(() => setOnboarded(false));
|
|
311
|
+
fetchAuthStatus()
|
|
312
|
+
.then((data) => setAuthEnabled(!!data.authEnabled))
|
|
313
|
+
.catch(() => {});
|
|
298
314
|
}, []);
|
|
299
315
|
|
|
300
316
|
useEffect(() => {
|
|
301
|
-
|
|
302
|
-
localStorage.setItem(kUiTabStorageKey, tab);
|
|
303
|
-
} catch {}
|
|
317
|
+
history.replaceState(null, "", `#${tab}`);
|
|
304
318
|
}, [tab]);
|
|
305
319
|
|
|
306
320
|
useEffect(() => {
|
|
@@ -397,6 +411,34 @@ function App() {
|
|
|
397
411
|
<div class="sidebar-brand">
|
|
398
412
|
<img src="./img/logo.svg" alt="" width="20" height="20" />
|
|
399
413
|
<span><span style="color: var(--accent)">alpha</span>claw</span>
|
|
414
|
+
${authEnabled && html`
|
|
415
|
+
<div class="brand-menu" ref=${menuRef}>
|
|
416
|
+
<button
|
|
417
|
+
class="brand-menu-trigger"
|
|
418
|
+
onclick=${() => setMenuOpen((o) => !o)}
|
|
419
|
+
aria-label="Menu"
|
|
420
|
+
>
|
|
421
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
422
|
+
<circle cx="8" cy="3" r="1.5" />
|
|
423
|
+
<circle cx="8" cy="8" r="1.5" />
|
|
424
|
+
<circle cx="8" cy="13" r="1.5" />
|
|
425
|
+
</svg>
|
|
426
|
+
</button>
|
|
427
|
+
${menuOpen && html`
|
|
428
|
+
<div class="brand-dropdown">
|
|
429
|
+
<a
|
|
430
|
+
href="#"
|
|
431
|
+
onclick=${async (e) => {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
setMenuOpen(false);
|
|
434
|
+
await logout();
|
|
435
|
+
window.location.href = "/login.html";
|
|
436
|
+
}}
|
|
437
|
+
>Log out</a>
|
|
438
|
+
</div>
|
|
439
|
+
`}
|
|
440
|
+
</div>
|
|
441
|
+
`}
|
|
400
442
|
</div>
|
|
401
443
|
<div class="sidebar-label">Setup</div>
|
|
402
444
|
<nav class="sidebar-nav">
|
|
@@ -429,7 +471,10 @@ function App() {
|
|
|
429
471
|
<div class="app-content">
|
|
430
472
|
<div class="max-w-2xl w-full mx-auto space-y-4">
|
|
431
473
|
<div style=${{ display: tab === "general" ? "" : "none" }}>
|
|
432
|
-
<${GeneralTab}
|
|
474
|
+
<${GeneralTab}
|
|
475
|
+
onSwitchTab=${setTab}
|
|
476
|
+
isActive=${tab === "general"}
|
|
477
|
+
/>
|
|
433
478
|
</div>
|
|
434
479
|
<div style=${{ display: tab === "models" ? "" : "none" }}>
|
|
435
480
|
<${Models} />
|
|
@@ -4,6 +4,10 @@ import { Badge } from './badge.js';
|
|
|
4
4
|
const html = htm.bind(h);
|
|
5
5
|
|
|
6
6
|
const ALL_CHANNELS = ['telegram', 'discord'];
|
|
7
|
+
const kChannelMeta = {
|
|
8
|
+
telegram: { label: 'Telegram', iconSrc: '/assets/icons/telegram.svg' },
|
|
9
|
+
discord: { label: 'Discord', iconSrc: '/assets/icons/discord.svg' },
|
|
10
|
+
};
|
|
7
11
|
|
|
8
12
|
export function Channels({ channels, onSwitchTab }) {
|
|
9
13
|
return html`
|
|
@@ -12,6 +16,7 @@ export function Channels({ channels, onSwitchTab }) {
|
|
|
12
16
|
<div class="space-y-2">
|
|
13
17
|
${channels ? ALL_CHANNELS.map(ch => {
|
|
14
18
|
const info = channels[ch];
|
|
19
|
+
const channelMeta = kChannelMeta[ch] || { label: ch.charAt(0).toUpperCase() + ch.slice(1), iconSrc: '' };
|
|
15
20
|
let badge;
|
|
16
21
|
if (!info) {
|
|
17
22
|
badge = html`<a
|
|
@@ -25,7 +30,12 @@ export function Channels({ channels, onSwitchTab }) {
|
|
|
25
30
|
badge = html`<${Badge} tone="warning">Awaiting pairing</${Badge}>`;
|
|
26
31
|
}
|
|
27
32
|
return html`<div class="flex justify-between items-center py-1.5">
|
|
28
|
-
<span class="font-medium text-sm
|
|
33
|
+
<span class="font-medium text-sm flex items-center gap-2">
|
|
34
|
+
${channelMeta.iconSrc
|
|
35
|
+
? html`<img src=${channelMeta.iconSrc} alt="" class="w-4 h-4 rounded-sm" aria-hidden="true" />`
|
|
36
|
+
: null}
|
|
37
|
+
${channelMeta.label}
|
|
38
|
+
</span>
|
|
29
39
|
${badge}
|
|
30
40
|
</div>`;
|
|
31
41
|
}) : html`<div class="text-gray-500 text-sm text-center py-2">Loading...</div>`}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useState, useEffect, useCallback } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { useState, useEffect, useCallback, useRef } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { fetchEnvVars, saveEnvVars, restartGateway } from "../lib/api.js";
|
|
5
5
|
import { showToast } from "./toast.js";
|
|
@@ -14,6 +14,16 @@ const kGroupLabels = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
const kGroupOrder = ["github", "channels", "tools", "custom"];
|
|
17
|
+
const normalizeEnvVarKey = (raw) => raw.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
18
|
+
const getVarsSignature = (items) =>
|
|
19
|
+
JSON.stringify(
|
|
20
|
+
(items || [])
|
|
21
|
+
.map((v) => ({
|
|
22
|
+
key: String(v?.key || ""),
|
|
23
|
+
value: String(v?.value || ""),
|
|
24
|
+
}))
|
|
25
|
+
.sort((a, b) => a.key.localeCompare(b.key)),
|
|
26
|
+
);
|
|
17
27
|
|
|
18
28
|
const kHintByKey = {
|
|
19
29
|
ANTHROPIC_API_KEY: html`from <a href="https://console.anthropic.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.anthropic.com</a>`,
|
|
@@ -72,17 +82,21 @@ const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
|
72
82
|
|
|
73
83
|
export const Envars = () => {
|
|
74
84
|
const [vars, setVars] = useState([]);
|
|
85
|
+
const [reservedKeys, setReservedKeys] = useState(() => new Set());
|
|
75
86
|
const [dirty, setDirty] = useState(false);
|
|
76
87
|
const [saving, setSaving] = useState(false);
|
|
77
88
|
const [restartingGateway, setRestartingGateway] = useState(false);
|
|
78
89
|
const [restartRequired, setRestartRequired] = useState(false);
|
|
79
90
|
const [newKey, setNewKey] = useState("");
|
|
91
|
+
const baselineSignatureRef = useRef("[]");
|
|
80
92
|
|
|
81
93
|
const load = useCallback(async () => {
|
|
82
94
|
try {
|
|
83
95
|
const data = await fetchEnvVars();
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
const nextVars = data.vars || [];
|
|
97
|
+
baselineSignatureRef.current = getVarsSignature(nextVars);
|
|
98
|
+
setVars(nextVars);
|
|
99
|
+
setReservedKeys(new Set(data.reservedKeys || []));
|
|
86
100
|
setRestartRequired(!!data.restartRequired);
|
|
87
101
|
} catch (err) {
|
|
88
102
|
console.error("Failed to load env vars:", err);
|
|
@@ -93,14 +107,16 @@ export const Envars = () => {
|
|
|
93
107
|
load();
|
|
94
108
|
}, [load]);
|
|
95
109
|
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
setDirty(getVarsSignature(vars) !== baselineSignatureRef.current);
|
|
112
|
+
}, [vars]);
|
|
113
|
+
|
|
96
114
|
const handleChange = (key, value) => {
|
|
97
115
|
setVars((prev) => prev.map((v) => (v.key === key ? { ...v, value } : v)));
|
|
98
|
-
setDirty(true);
|
|
99
116
|
};
|
|
100
117
|
|
|
101
118
|
const handleDelete = (key) => {
|
|
102
119
|
setVars((prev) => prev.filter((v) => v.key !== key));
|
|
103
|
-
setDirty(true);
|
|
104
120
|
};
|
|
105
121
|
|
|
106
122
|
const handleSave = async () => {
|
|
@@ -119,6 +135,7 @@ export const Envars = () => {
|
|
|
119
135
|
: "Environment variables saved",
|
|
120
136
|
"success",
|
|
121
137
|
);
|
|
138
|
+
baselineSignatureRef.current = getVarsSignature(vars);
|
|
122
139
|
setDirty(false);
|
|
123
140
|
} catch (err) {
|
|
124
141
|
showToast("Failed to save: " + err.message, "error");
|
|
@@ -163,11 +180,16 @@ export const Envars = () => {
|
|
|
163
180
|
|
|
164
181
|
const addVars = (pairs) => {
|
|
165
182
|
let added = 0;
|
|
183
|
+
const blocked = [];
|
|
166
184
|
setVars((prev) => {
|
|
167
185
|
const next = [...prev];
|
|
168
186
|
for (const { key: rawKey, value } of pairs) {
|
|
169
|
-
const key = rawKey
|
|
187
|
+
const key = normalizeEnvVarKey(rawKey);
|
|
170
188
|
if (!key) continue;
|
|
189
|
+
if (reservedKeys.has(key)) {
|
|
190
|
+
blocked.push(key);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
171
193
|
const existing = next.find((v) => v.key === key);
|
|
172
194
|
if (existing) {
|
|
173
195
|
existing.value = value;
|
|
@@ -186,8 +208,7 @@ export const Envars = () => {
|
|
|
186
208
|
}
|
|
187
209
|
return next;
|
|
188
210
|
});
|
|
189
|
-
|
|
190
|
-
return added;
|
|
211
|
+
return { added, blocked };
|
|
191
212
|
};
|
|
192
213
|
|
|
193
214
|
const handlePaste = (e, fallbackField) => {
|
|
@@ -195,10 +216,19 @@ export const Envars = () => {
|
|
|
195
216
|
const pairs = parsePaste(text);
|
|
196
217
|
if (pairs.length > 1) {
|
|
197
218
|
e.preventDefault();
|
|
198
|
-
const added = addVars(pairs);
|
|
219
|
+
const { added, blocked } = addVars(pairs);
|
|
199
220
|
setNewKey("");
|
|
200
221
|
setNewVal("");
|
|
201
|
-
|
|
222
|
+
if (blocked.length) {
|
|
223
|
+
const uniqueBlocked = Array.from(new Set(blocked));
|
|
224
|
+
showToast(
|
|
225
|
+
`Reserved vars can't be added: ${uniqueBlocked.join(", ")}`,
|
|
226
|
+
"error",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (added) {
|
|
230
|
+
showToast(`Added ${added} variable${added !== 1 ? "s" : ""}`, "success");
|
|
231
|
+
}
|
|
202
232
|
return;
|
|
203
233
|
}
|
|
204
234
|
if (pairs.length === 1) {
|
|
@@ -230,11 +260,12 @@ export const Envars = () => {
|
|
|
230
260
|
};
|
|
231
261
|
|
|
232
262
|
const handleAddVar = () => {
|
|
233
|
-
const key = newKey
|
|
234
|
-
.trim()
|
|
235
|
-
.toUpperCase()
|
|
236
|
-
.replace(/[^A-Z0-9_]/g, "_");
|
|
263
|
+
const key = normalizeEnvVarKey(newKey);
|
|
237
264
|
if (!key) return;
|
|
265
|
+
if (reservedKeys.has(key)) {
|
|
266
|
+
showToast(`Reserved var can't be added: ${key}`, "error");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
238
269
|
addVars([{ key, value: newVal }]);
|
|
239
270
|
setNewKey("");
|
|
240
271
|
setNewVal("");
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../lib/api.js";
|
|
14
14
|
import { showToast } from "./toast.js";
|
|
15
15
|
import { Badge } from "./badge.js";
|
|
16
|
+
import { SecretInput } from "./secret-input.js";
|
|
16
17
|
import {
|
|
17
18
|
getModelProvider,
|
|
18
19
|
getAuthProviderFromModelProvider,
|
|
@@ -274,12 +275,12 @@ export const Models = () => {
|
|
|
274
275
|
const renderCredentialField = (field) => html`
|
|
275
276
|
<div class="space-y-1">
|
|
276
277
|
<label class="text-xs font-medium text-gray-400">${field.label}</label>
|
|
277
|
-
|
|
278
|
-
type="password"
|
|
279
|
-
placeholder=${field.placeholder || ""}
|
|
278
|
+
<${SecretInput}
|
|
280
279
|
value=${getKeyVal(envVars, field.key)}
|
|
281
280
|
onInput=${(e) => setEnvValue(field.key, e.target.value)}
|
|
282
|
-
|
|
281
|
+
placeholder=${field.placeholder || ""}
|
|
282
|
+
isSecret=${!field.isText}
|
|
283
|
+
inputClass="flex-1 w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
283
284
|
/>
|
|
284
285
|
<p class="text-xs text-gray-600">${field.hint}</p>
|
|
285
286
|
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const getPreferredPairingChannel = (vals = {}) => {
|
|
2
|
+
if (vals.TELEGRAM_BOT_TOKEN) return "telegram";
|
|
3
|
+
if (vals.DISCORD_BOT_TOKEN) return "discord";
|
|
4
|
+
return "";
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const isChannelPaired = (channels = {}, channel = "") => {
|
|
8
|
+
if (!channel) return false;
|
|
9
|
+
const info = channels?.[channel];
|
|
10
|
+
if (!info) return false;
|
|
11
|
+
return info.status === "paired" && Number(info.paired || 0) > 0;
|
|
12
|
+
};
|
|
@@ -4,6 +4,20 @@ import { kAllAiAuthFields } from "../../lib/model-config.js";
|
|
|
4
4
|
|
|
5
5
|
const html = htm.bind(h);
|
|
6
6
|
|
|
7
|
+
export const normalizeGithubRepoInput = (repoInput) =>
|
|
8
|
+
String(repoInput || "")
|
|
9
|
+
.trim()
|
|
10
|
+
.replace(/^git@github\.com:/, "")
|
|
11
|
+
.replace(/^https:\/\/github\.com\//, "")
|
|
12
|
+
.replace(/\.git$/, "");
|
|
13
|
+
|
|
14
|
+
export const isValidGithubRepoInput = (repoInput) => {
|
|
15
|
+
const cleaned = normalizeGithubRepoInput(repoInput);
|
|
16
|
+
if (!cleaned) return false;
|
|
17
|
+
const parts = cleaned.split("/").filter(Boolean);
|
|
18
|
+
return parts.length === 2 && !parts.some((part) => /\s/.test(part));
|
|
19
|
+
};
|
|
20
|
+
|
|
7
21
|
export const kWelcomeGroups = [
|
|
8
22
|
{
|
|
9
23
|
id: "ai",
|
|
@@ -39,7 +53,10 @@ export const kWelcomeGroups = [
|
|
|
39
53
|
placeholder: "ghp_...",
|
|
40
54
|
},
|
|
41
55
|
],
|
|
42
|
-
validate: (vals) =>
|
|
56
|
+
validate: (vals) =>
|
|
57
|
+
!!(
|
|
58
|
+
vals.GITHUB_TOKEN && isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO)
|
|
59
|
+
),
|
|
43
60
|
},
|
|
44
61
|
{
|
|
45
62
|
id: "channels",
|