@chrysb/alphaclaw 0.1.20 → 0.1.21
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 +30 -8
- 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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { Badge } from "../badge.js";
|
|
5
|
+
|
|
6
|
+
const html = htm.bind(h);
|
|
7
|
+
|
|
8
|
+
const kChannelMeta = {
|
|
9
|
+
telegram: {
|
|
10
|
+
label: "Telegram",
|
|
11
|
+
iconSrc: "/assets/icons/telegram.svg",
|
|
12
|
+
},
|
|
13
|
+
discord: {
|
|
14
|
+
label: "Discord",
|
|
15
|
+
iconSrc: "/assets/icons/discord.svg",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const PairingRow = ({ pairing, onApprove, onReject }) => {
|
|
20
|
+
const [busyAction, setBusyAction] = useState("");
|
|
21
|
+
|
|
22
|
+
const handleApprove = async () => {
|
|
23
|
+
setBusyAction("approve");
|
|
24
|
+
try {
|
|
25
|
+
await onApprove(pairing.id, pairing.channel);
|
|
26
|
+
} finally {
|
|
27
|
+
setBusyAction("");
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleReject = async () => {
|
|
32
|
+
setBusyAction("reject");
|
|
33
|
+
try {
|
|
34
|
+
await onReject(pairing.id, pairing.channel);
|
|
35
|
+
} finally {
|
|
36
|
+
setBusyAction("");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return html`
|
|
41
|
+
<div class="bg-black/30 rounded-lg p-3 mb-2">
|
|
42
|
+
<div class="flex items-center justify-between gap-2 mb-2">
|
|
43
|
+
<div class="font-medium text-sm">
|
|
44
|
+
${pairing.code || pairing.id || "Pending request"}
|
|
45
|
+
</div>
|
|
46
|
+
<span class="text-[11px] px-2 py-0.5 rounded-full border border-border text-gray-400">
|
|
47
|
+
Request
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
<p class="text-xs text-gray-500 mb-3">
|
|
51
|
+
Approve to connect this account and finish setup.
|
|
52
|
+
</p>
|
|
53
|
+
<div class="flex gap-2">
|
|
54
|
+
<button
|
|
55
|
+
onclick=${handleApprove}
|
|
56
|
+
disabled=${!!busyAction}
|
|
57
|
+
class="ac-btn-green text-xs font-medium px-3 py-1.5 rounded-lg ${busyAction ? "opacity-60 cursor-not-allowed" : ""}"
|
|
58
|
+
>
|
|
59
|
+
${busyAction === "approve" ? "Approving..." : "Approve"}
|
|
60
|
+
</button>
|
|
61
|
+
<button
|
|
62
|
+
onclick=${handleReject}
|
|
63
|
+
disabled=${!!busyAction}
|
|
64
|
+
class="bg-gray-800 text-gray-300 text-xs px-3 py-1.5 rounded-lg hover:bg-gray-700 ${busyAction ? "opacity-60 cursor-not-allowed" : ""}"
|
|
65
|
+
>
|
|
66
|
+
${busyAction === "reject" ? "Rejecting..." : "Reject"}
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const WelcomePairingStep = ({
|
|
74
|
+
channel,
|
|
75
|
+
pairings,
|
|
76
|
+
channels,
|
|
77
|
+
loading,
|
|
78
|
+
error,
|
|
79
|
+
onApprove,
|
|
80
|
+
onReject,
|
|
81
|
+
canFinish,
|
|
82
|
+
onContinue,
|
|
83
|
+
}) => {
|
|
84
|
+
const channelMeta = kChannelMeta[channel] || {
|
|
85
|
+
label: channel ? channel.charAt(0).toUpperCase() + channel.slice(1) : "Channel",
|
|
86
|
+
iconSrc: "",
|
|
87
|
+
};
|
|
88
|
+
const channelInfo = channels?.[channel];
|
|
89
|
+
|
|
90
|
+
if (!channel) {
|
|
91
|
+
return html`
|
|
92
|
+
<div class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm">
|
|
93
|
+
Missing channel configuration. Go back and add a Telegram or Discord bot token.
|
|
94
|
+
</div>
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (canFinish) {
|
|
99
|
+
return html`
|
|
100
|
+
<div class="min-h-[300px] pb-6 px-6 flex flex-col">
|
|
101
|
+
<div class="flex-1 flex items-center justify-center text-center">
|
|
102
|
+
<div class="space-y-3 max-w-xl mx-auto">
|
|
103
|
+
<p class="text-sm font-medium text-green-300 mb-12">🎉 Setup complete</p>
|
|
104
|
+
<p class="text-xs text-gray-300">
|
|
105
|
+
Your ${channelMeta.label} channel is connected. You can switch to ${channelMeta.label} and start using your agent now.
|
|
106
|
+
</p>
|
|
107
|
+
<p class="text-xs text-gray-500 font-normal opacity-85">
|
|
108
|
+
Continue to the dashboard to explore extras like Google Workspace and additional integrations.
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<button
|
|
113
|
+
onclick=${onContinue}
|
|
114
|
+
class="w-full max-w-xl mx-auto text-sm font-medium px-4 py-2.5 rounded-xl transition-all ac-btn-cyan mt-3"
|
|
115
|
+
>
|
|
116
|
+
Continue to dashboard
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return html`
|
|
123
|
+
<div class="min-h-[300px] pb-6 flex flex-col gap-3">
|
|
124
|
+
<div class="flex items-center justify-end gap-2">
|
|
125
|
+
<${Badge} tone="warning"
|
|
126
|
+
>${loading
|
|
127
|
+
? "Checking..."
|
|
128
|
+
: pairings.length > 0
|
|
129
|
+
? "Pairing request detected"
|
|
130
|
+
: "Awaiting pairing"}</${Badge}
|
|
131
|
+
>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
${pairings.length > 0
|
|
135
|
+
? html`<div class="flex-1 flex items-center">
|
|
136
|
+
<div class="w-full">
|
|
137
|
+
${pairings.map(
|
|
138
|
+
(pairing) =>
|
|
139
|
+
html`<${PairingRow}
|
|
140
|
+
key=${pairing.id}
|
|
141
|
+
pairing=${pairing}
|
|
142
|
+
onApprove=${onApprove}
|
|
143
|
+
onReject=${onReject}
|
|
144
|
+
/>`,
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</div>`
|
|
148
|
+
: html`<div class="flex-1 flex items-center justify-center text-center py-4">
|
|
149
|
+
<div class="space-y-4">
|
|
150
|
+
${channelMeta.iconSrc
|
|
151
|
+
? html`<img
|
|
152
|
+
src=${channelMeta.iconSrc}
|
|
153
|
+
alt=${channelMeta.label}
|
|
154
|
+
class="w-8 h-8 mx-auto rounded-md"
|
|
155
|
+
/>`
|
|
156
|
+
: null}
|
|
157
|
+
<p class="text-gray-300 text-sm">
|
|
158
|
+
Send a message to your ${channelMeta.label} bot
|
|
159
|
+
</p>
|
|
160
|
+
<p class="text-gray-600 text-xs">
|
|
161
|
+
The pairing request will appear here in 5-10 seconds
|
|
162
|
+
</p>
|
|
163
|
+
</div>
|
|
164
|
+
</div>`}
|
|
165
|
+
|
|
166
|
+
${error
|
|
167
|
+
? html`<div class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm">
|
|
168
|
+
${error}
|
|
169
|
+
</div>`
|
|
170
|
+
: null}
|
|
171
|
+
</div>
|
|
172
|
+
`;
|
|
173
|
+
};
|
|
@@ -1,45 +1,117 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
2
3
|
import htm from "https://esm.sh/htm";
|
|
3
4
|
|
|
4
5
|
const html = htm.bind(h);
|
|
6
|
+
const kSetupTips = [
|
|
7
|
+
{
|
|
8
|
+
label: "🛡️ Safety tip",
|
|
9
|
+
text: "Be careful what you give access to. Read access is always safer than write access.",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
label: "🧠 Best practice",
|
|
13
|
+
text: "Trust but verify. Your agent may not always know what it's doing, so check the results.",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: "💡 Idea",
|
|
17
|
+
text: "Ask your agent to create a morning briefing for you.",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: "🧠 Best practice",
|
|
21
|
+
text: "Ask your agent to review its own code and make sure it's doing what you want it to do.",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "💡 Idea",
|
|
25
|
+
text: "Tell your agent to review the latest news and provide a summary.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: "🛡️ Safety tip",
|
|
29
|
+
text: "Be incredibly careful installing skills from the internet - they may contain malicious code.",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
5
32
|
|
|
6
|
-
export const WelcomeSetupStep = ({ error, loading, onRetry }) =>
|
|
7
|
-
|
|
8
|
-
<svg
|
|
9
|
-
class="animate-spin h-8 w-8 text-white"
|
|
10
|
-
viewBox="0 0 24 24"
|
|
11
|
-
fill="none"
|
|
12
|
-
>
|
|
13
|
-
<circle
|
|
14
|
-
class="opacity-25"
|
|
15
|
-
cx="12"
|
|
16
|
-
cy="12"
|
|
17
|
-
r="10"
|
|
18
|
-
stroke="currentColor"
|
|
19
|
-
stroke-width="4"
|
|
20
|
-
/>
|
|
21
|
-
<path
|
|
22
|
-
class="opacity-75"
|
|
23
|
-
fill="currentColor"
|
|
24
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
25
|
-
/>
|
|
26
|
-
</svg>
|
|
27
|
-
<h3 class="text-lg font-semibold text-white">Initializing OpenClaw...</h3>
|
|
28
|
-
<p class="text-sm text-gray-500">This could take 10-15 seconds</p>
|
|
29
|
-
</div>
|
|
33
|
+
export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
|
|
34
|
+
const [tipIndex, setTipIndex] = useState(0);
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (error || !loading) return;
|
|
38
|
+
const timer = setInterval(() => {
|
|
39
|
+
setTipIndex((idx) => (idx + 1) % kSetupTips.length);
|
|
40
|
+
}, 5200);
|
|
41
|
+
return () => clearInterval(timer);
|
|
42
|
+
}, [error, loading]);
|
|
43
|
+
|
|
44
|
+
if (error) {
|
|
45
|
+
return html`
|
|
46
|
+
<div class="py-4 flex flex-col items-center text-center gap-3">
|
|
47
|
+
<h3 class="text-lg font-semibold text-white">Setup failed</h3>
|
|
48
|
+
<p class="text-sm text-gray-500">Fix the values and try again.</p>
|
|
49
|
+
</div>
|
|
50
|
+
<div
|
|
51
|
+
class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
|
|
52
|
+
>
|
|
33
53
|
${error}
|
|
34
54
|
</div>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
<div class="grid grid-cols-2 gap-2">
|
|
56
|
+
<button
|
|
57
|
+
onclick=${onBack}
|
|
58
|
+
disabled=${loading}
|
|
59
|
+
class="w-full text-sm font-medium px-4 py-3 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500 ${loading
|
|
60
|
+
? "opacity-60 cursor-not-allowed"
|
|
61
|
+
: ""}"
|
|
62
|
+
>
|
|
63
|
+
Back
|
|
64
|
+
</button>
|
|
65
|
+
<button
|
|
66
|
+
onclick=${onRetry}
|
|
67
|
+
disabled=${loading}
|
|
68
|
+
class="w-full text-sm font-medium px-4 py-3 rounded-xl transition-all ${loading
|
|
69
|
+
? "bg-gray-800 text-gray-500 cursor-not-allowed"
|
|
70
|
+
: "bg-white text-black hover:opacity-85"}"
|
|
71
|
+
>
|
|
72
|
+
${loading ? "Retrying..." : "Retry"}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const currentTip = kSetupTips[tipIndex];
|
|
79
|
+
|
|
80
|
+
return html`
|
|
81
|
+
<div class="min-h-[320px] py-4 flex flex-col">
|
|
82
|
+
<div
|
|
83
|
+
class="flex-1 flex flex-col items-center justify-center text-center gap-4"
|
|
84
|
+
>
|
|
85
|
+
<svg
|
|
86
|
+
class="animate-spin h-8 w-8 text-white"
|
|
87
|
+
viewBox="0 0 24 24"
|
|
88
|
+
fill="none"
|
|
89
|
+
>
|
|
90
|
+
<circle
|
|
91
|
+
class="opacity-25"
|
|
92
|
+
cx="12"
|
|
93
|
+
cy="12"
|
|
94
|
+
r="10"
|
|
95
|
+
stroke="currentColor"
|
|
96
|
+
stroke-width="4"
|
|
97
|
+
/>
|
|
98
|
+
<path
|
|
99
|
+
class="opacity-75"
|
|
100
|
+
fill="currentColor"
|
|
101
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
102
|
+
/>
|
|
103
|
+
</svg>
|
|
104
|
+
<h3 class="text-lg font-semibold text-white">
|
|
105
|
+
Initializing OpenClaw...
|
|
106
|
+
</h3>
|
|
107
|
+
<p class="text-sm text-gray-500">This could take 10-15 seconds</p>
|
|
108
|
+
</div>
|
|
109
|
+
<div
|
|
110
|
+
class="mt-3 bg-black/20 border border-border rounded-lg px-3 py-2 text-xs text-gray-500"
|
|
41
111
|
>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
112
|
+
<span class="text-gray-400">${currentTip.label}: </span>
|
|
113
|
+
${currentTip.text}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
`;
|
|
117
|
+
};
|
|
@@ -13,6 +13,7 @@ const html = htm.bind(h);
|
|
|
13
13
|
export const SecretInput = ({
|
|
14
14
|
value = "",
|
|
15
15
|
onInput,
|
|
16
|
+
onBlur,
|
|
16
17
|
placeholder = "",
|
|
17
18
|
inputClass = "",
|
|
18
19
|
disabled = false,
|
|
@@ -28,6 +29,7 @@ export const SecretInput = ({
|
|
|
28
29
|
value=${value}
|
|
29
30
|
placeholder=${placeholder}
|
|
30
31
|
onInput=${onInput}
|
|
32
|
+
onBlur=${onBlur}
|
|
31
33
|
disabled=${disabled}
|
|
32
34
|
class=${inputClass}
|
|
33
35
|
/>
|
|
@@ -7,19 +7,36 @@ import {
|
|
|
7
7
|
fetchCodexStatus,
|
|
8
8
|
disconnectCodex,
|
|
9
9
|
exchangeCodexOAuth,
|
|
10
|
+
fetchStatus,
|
|
11
|
+
fetchPairings,
|
|
12
|
+
approvePairing,
|
|
13
|
+
rejectPairing,
|
|
10
14
|
} from "../lib/api.js";
|
|
15
|
+
import { usePolling } from "../hooks/usePolling.js";
|
|
11
16
|
import {
|
|
12
17
|
getModelProvider,
|
|
13
18
|
getFeaturedModels,
|
|
14
19
|
getVisibleAiFieldKeys,
|
|
15
20
|
} from "../lib/model-config.js";
|
|
16
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
kWelcomeGroups,
|
|
23
|
+
isValidGithubRepoInput,
|
|
24
|
+
} from "./onboarding/welcome-config.js";
|
|
17
25
|
import { WelcomeHeader } from "./onboarding/welcome-header.js";
|
|
18
26
|
import { WelcomeSetupStep } from "./onboarding/welcome-setup-step.js";
|
|
19
27
|
import { WelcomeFormStep } from "./onboarding/welcome-form-step.js";
|
|
28
|
+
import { WelcomePairingStep } from "./onboarding/welcome-pairing-step.js";
|
|
29
|
+
import {
|
|
30
|
+
getPreferredPairingChannel,
|
|
31
|
+
isChannelPaired,
|
|
32
|
+
} from "./onboarding/pairing-utils.js";
|
|
20
33
|
const html = htm.bind(h);
|
|
21
34
|
const kOnboardingStorageKey = "openclaw_setup";
|
|
22
35
|
const kOnboardingStepKey = "_step";
|
|
36
|
+
const kPairingChannelKey = "_pairingChannel";
|
|
37
|
+
const kMaxOnboardingVars = 64;
|
|
38
|
+
const kMaxEnvKeyLength = 128;
|
|
39
|
+
const kMaxEnvValueLength = 4096;
|
|
23
40
|
|
|
24
41
|
export const Welcome = ({ onComplete }) => {
|
|
25
42
|
const [initialSetupState] = useState(() => {
|
|
@@ -133,20 +150,47 @@ export const Welcome = ({ onComplete }) => {
|
|
|
133
150
|
: false;
|
|
134
151
|
|
|
135
152
|
const allValid = kWelcomeGroups.every((g) => g.validate(vals, { hasAi }));
|
|
136
|
-
const
|
|
153
|
+
const kSetupStepIndex = kWelcomeGroups.length;
|
|
154
|
+
const kPairingStepIndex = kSetupStepIndex + 1;
|
|
137
155
|
const [step, setStep] = useState(() => {
|
|
138
156
|
const parsedStep = Number.parseInt(
|
|
139
157
|
String(initialSetupState?.[kOnboardingStepKey] || ""),
|
|
140
158
|
10,
|
|
141
159
|
);
|
|
142
160
|
if (!Number.isFinite(parsedStep)) return 0;
|
|
143
|
-
return Math.max(0, Math.min(
|
|
161
|
+
return Math.max(0, Math.min(kPairingStepIndex, parsedStep));
|
|
144
162
|
});
|
|
145
|
-
const
|
|
146
|
-
const
|
|
163
|
+
const [pairingError, setPairingError] = useState(null);
|
|
164
|
+
const [pairingComplete, setPairingComplete] = useState(false);
|
|
165
|
+
const isSetupStep = step === kSetupStepIndex;
|
|
166
|
+
const isPairingStep = step === kPairingStepIndex;
|
|
167
|
+
const activeGroup = step < kSetupStepIndex ? kWelcomeGroups[step] : null;
|
|
147
168
|
const currentGroupValid = activeGroup
|
|
148
169
|
? activeGroup.validate(vals, { hasAi })
|
|
149
170
|
: false;
|
|
171
|
+
const selectedPairingChannel = String(
|
|
172
|
+
vals[kPairingChannelKey] || getPreferredPairingChannel(vals),
|
|
173
|
+
);
|
|
174
|
+
const pairingStatusPoll = usePolling(fetchStatus, 3000, {
|
|
175
|
+
enabled: isPairingStep,
|
|
176
|
+
});
|
|
177
|
+
const pairingRequestsPoll = usePolling(
|
|
178
|
+
async () => {
|
|
179
|
+
const payload = await fetchPairings();
|
|
180
|
+
const allPending = payload.pending || [];
|
|
181
|
+
return allPending.filter((p) => p.channel === selectedPairingChannel);
|
|
182
|
+
},
|
|
183
|
+
1000,
|
|
184
|
+
{ enabled: isPairingStep && !!selectedPairingChannel },
|
|
185
|
+
);
|
|
186
|
+
const pairingChannels = pairingStatusPoll.data?.channels || {};
|
|
187
|
+
const canFinishPairing = isChannelPaired(pairingChannels, selectedPairingChannel);
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (isPairingStep && canFinishPairing) {
|
|
191
|
+
setPairingComplete(true);
|
|
192
|
+
}
|
|
193
|
+
}, [isPairingStep, canFinishPairing]);
|
|
150
194
|
|
|
151
195
|
useEffect(() => {
|
|
152
196
|
localStorage.setItem(
|
|
@@ -218,21 +262,59 @@ export const Welcome = ({ onComplete }) => {
|
|
|
218
262
|
|
|
219
263
|
const handleSubmit = async () => {
|
|
220
264
|
if (!allValid || loading) return;
|
|
221
|
-
|
|
265
|
+
const vars = Object.entries(vals)
|
|
266
|
+
.filter(
|
|
267
|
+
([key]) => key !== "MODEL_KEY" && !String(key || "").startsWith("_"),
|
|
268
|
+
)
|
|
269
|
+
.filter(([, v]) => v)
|
|
270
|
+
.map(([key, value]) => ({ key, value }));
|
|
271
|
+
const preflightError = (() => {
|
|
272
|
+
if (!vals.MODEL_KEY || !String(vals.MODEL_KEY).includes("/")) {
|
|
273
|
+
return "A model selection is required";
|
|
274
|
+
}
|
|
275
|
+
if (vars.length > kMaxOnboardingVars) {
|
|
276
|
+
return `Too many environment variables (max ${kMaxOnboardingVars})`;
|
|
277
|
+
}
|
|
278
|
+
for (const entry of vars) {
|
|
279
|
+
const key = String(entry?.key || "");
|
|
280
|
+
const value = String(entry?.value || "");
|
|
281
|
+
if (!key) return "Each variable must include a key";
|
|
282
|
+
if (key.length > kMaxEnvKeyLength) {
|
|
283
|
+
return `Variable key is too long: ${key.slice(0, 32)}...`;
|
|
284
|
+
}
|
|
285
|
+
if (value.length > kMaxEnvValueLength) {
|
|
286
|
+
return `Value too long for ${key} (max ${kMaxEnvValueLength} chars)`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!vals.GITHUB_TOKEN || !isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO)) {
|
|
290
|
+
return 'GITHUB_WORKSPACE_REPO must be in "owner/repo" format.';
|
|
291
|
+
}
|
|
292
|
+
return "";
|
|
293
|
+
})();
|
|
294
|
+
if (preflightError) {
|
|
295
|
+
setError(preflightError);
|
|
296
|
+
setStep(Math.max(0, kWelcomeGroups.findIndex((g) => g.id === "github")));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
setStep(kSetupStepIndex);
|
|
222
300
|
setLoading(true);
|
|
223
301
|
setError(null);
|
|
302
|
+
setPairingError(null);
|
|
224
303
|
|
|
225
304
|
try {
|
|
226
|
-
const vars = Object.entries(vals)
|
|
227
|
-
.filter(
|
|
228
|
-
([key]) => key !== "MODEL_KEY" && !String(key || "").startsWith("_"),
|
|
229
|
-
)
|
|
230
|
-
.filter(([, v]) => v)
|
|
231
|
-
.map(([key, value]) => ({ key, value }));
|
|
232
305
|
const result = await runOnboard(vars, vals.MODEL_KEY);
|
|
233
306
|
if (!result.ok) throw new Error(result.error || "Onboarding failed");
|
|
234
|
-
|
|
235
|
-
|
|
307
|
+
const pairingChannel = getPreferredPairingChannel(vals);
|
|
308
|
+
if (!pairingChannel) {
|
|
309
|
+
throw new Error("No Telegram or Discord bot token configured for pairing.");
|
|
310
|
+
}
|
|
311
|
+
setVals((prev) => ({
|
|
312
|
+
...prev,
|
|
313
|
+
[kPairingChannelKey]: pairingChannel,
|
|
314
|
+
}));
|
|
315
|
+
setLoading(false);
|
|
316
|
+
setStep(kPairingStepIndex);
|
|
317
|
+
setPairingComplete(false);
|
|
236
318
|
} catch (err) {
|
|
237
319
|
console.error("Onboard error:", err);
|
|
238
320
|
setError(err.message);
|
|
@@ -240,10 +322,43 @@ export const Welcome = ({ onComplete }) => {
|
|
|
240
322
|
}
|
|
241
323
|
};
|
|
242
324
|
|
|
325
|
+
const handlePairingApprove = async (id, channel) => {
|
|
326
|
+
try {
|
|
327
|
+
setPairingError(null);
|
|
328
|
+
const result = await approvePairing(id, channel);
|
|
329
|
+
if (!result.ok) throw new Error(result.error || "Could not approve pairing");
|
|
330
|
+
setPairingComplete(true);
|
|
331
|
+
pairingRequestsPoll.refresh();
|
|
332
|
+
pairingStatusPoll.refresh();
|
|
333
|
+
} catch (err) {
|
|
334
|
+
setPairingError(err.message || "Could not approve pairing");
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const handlePairingReject = async (id, channel) => {
|
|
339
|
+
try {
|
|
340
|
+
setPairingError(null);
|
|
341
|
+
const result = await rejectPairing(id, channel);
|
|
342
|
+
if (!result.ok) throw new Error(result.error || "Could not reject pairing");
|
|
343
|
+
pairingRequestsPoll.refresh();
|
|
344
|
+
} catch (err) {
|
|
345
|
+
setPairingError(err.message || "Could not reject pairing");
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const finishOnboarding = () => {
|
|
350
|
+
localStorage.removeItem(kOnboardingStorageKey);
|
|
351
|
+
onComplete();
|
|
352
|
+
};
|
|
353
|
+
|
|
243
354
|
const goBack = () => {
|
|
244
355
|
if (isSetupStep) return;
|
|
245
356
|
setStep((prev) => Math.max(0, prev - 1));
|
|
246
357
|
};
|
|
358
|
+
const goBackFromSetupError = () => {
|
|
359
|
+
setLoading(false);
|
|
360
|
+
setStep(kWelcomeGroups.length - 1);
|
|
361
|
+
};
|
|
247
362
|
|
|
248
363
|
const goNext = () => {
|
|
249
364
|
if (!activeGroup || !currentGroupValid) return;
|
|
@@ -252,8 +367,14 @@ export const Welcome = ({ onComplete }) => {
|
|
|
252
367
|
|
|
253
368
|
const activeStepLabel = isSetupStep
|
|
254
369
|
? "Initializing"
|
|
255
|
-
:
|
|
256
|
-
|
|
370
|
+
: isPairingStep
|
|
371
|
+
? "Pairing"
|
|
372
|
+
: activeGroup?.title || "Setup";
|
|
373
|
+
const stepNumber = isSetupStep
|
|
374
|
+
? kWelcomeGroups.length + 1
|
|
375
|
+
: isPairingStep
|
|
376
|
+
? kWelcomeGroups.length + 2
|
|
377
|
+
: step + 1;
|
|
257
378
|
|
|
258
379
|
return html`
|
|
259
380
|
<div class="max-w-lg w-full space-y-5">
|
|
@@ -261,10 +382,9 @@ export const Welcome = ({ onComplete }) => {
|
|
|
261
382
|
groups=${kWelcomeGroups}
|
|
262
383
|
step=${step}
|
|
263
384
|
isSetupStep=${isSetupStep}
|
|
385
|
+
isPairingStep=${isPairingStep}
|
|
264
386
|
stepNumber=${stepNumber}
|
|
265
387
|
activeStepLabel=${activeStepLabel}
|
|
266
|
-
vals=${vals}
|
|
267
|
-
hasAi=${hasAi}
|
|
268
388
|
/>
|
|
269
389
|
|
|
270
390
|
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
@@ -273,7 +393,20 @@ export const Welcome = ({ onComplete }) => {
|
|
|
273
393
|
error=${error}
|
|
274
394
|
loading=${loading}
|
|
275
395
|
onRetry=${handleSubmit}
|
|
396
|
+
onBack=${goBackFromSetupError}
|
|
276
397
|
/>`
|
|
398
|
+
: isPairingStep
|
|
399
|
+
? html`<${WelcomePairingStep}
|
|
400
|
+
channel=${selectedPairingChannel}
|
|
401
|
+
pairings=${pairingRequestsPoll.data || []}
|
|
402
|
+
channels=${pairingChannels}
|
|
403
|
+
loading=${!pairingStatusPoll.data}
|
|
404
|
+
error=${pairingError}
|
|
405
|
+
onApprove=${handlePairingApprove}
|
|
406
|
+
onReject=${handlePairingReject}
|
|
407
|
+
canFinish=${pairingComplete || canFinishPairing}
|
|
408
|
+
onContinue=${finishOnboarding}
|
|
409
|
+
/>`
|
|
277
410
|
: html`
|
|
278
411
|
<${WelcomeFormStep}
|
|
279
412
|
activeGroup=${activeGroup}
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const authFetch = async (url, opts = {}) => {
|
|
2
2
|
const res = await fetch(url, opts);
|
|
3
3
|
if (res.status === 401) {
|
|
4
|
+
try {
|
|
5
|
+
window.localStorage?.clear?.();
|
|
6
|
+
} catch {}
|
|
4
7
|
window.location.href = '/setup';
|
|
5
8
|
throw new Error('Unauthorized');
|
|
6
9
|
}
|
|
@@ -140,6 +143,16 @@ export async function rejectDevice(id) {
|
|
|
140
143
|
return res.json();
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
export const fetchAuthStatus = async () => {
|
|
147
|
+
const res = await authFetch('/api/auth/status');
|
|
148
|
+
return res.json();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const logout = async () => {
|
|
152
|
+
const res = await authFetch('/api/auth/logout', { method: 'POST' });
|
|
153
|
+
return res.json();
|
|
154
|
+
};
|
|
155
|
+
|
|
143
156
|
export async function fetchOnboardStatus() {
|
|
144
157
|
const res = await authFetch('/api/onboard/status');
|
|
145
158
|
return res.json();
|
|
@@ -203,5 +216,15 @@ export async function saveEnvVars(vars) {
|
|
|
203
216
|
headers: { 'Content-Type': 'application/json' },
|
|
204
217
|
body: JSON.stringify({ vars }),
|
|
205
218
|
});
|
|
206
|
-
|
|
219
|
+
const text = await res.text();
|
|
220
|
+
let data;
|
|
221
|
+
try {
|
|
222
|
+
data = text ? JSON.parse(text) : {};
|
|
223
|
+
} catch {
|
|
224
|
+
throw new Error(text || 'Could not parse env save response');
|
|
225
|
+
}
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
throw new Error(data.error || text || `HTTP ${res.status}`);
|
|
228
|
+
}
|
|
229
|
+
return data;
|
|
207
230
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
|
|
4
|
+
const html = htm.bind(h);
|
|
5
|
+
|
|
1
6
|
export const getModelProvider = (modelKey) => String(modelKey || "").split("/")[0] || "";
|
|
2
7
|
|
|
3
8
|
export const getAuthProviderFromModelProvider = (provider) =>
|
|
@@ -41,7 +46,14 @@ export const kProviderAuthFields = {
|
|
|
41
46
|
{
|
|
42
47
|
key: "ANTHROPIC_API_KEY",
|
|
43
48
|
label: "Anthropic API Key",
|
|
44
|
-
hint: "
|
|
49
|
+
hint: html`From${" "}
|
|
50
|
+
<a
|
|
51
|
+
href="https://console.anthropic.com"
|
|
52
|
+
target="_blank"
|
|
53
|
+
class="hover:underline"
|
|
54
|
+
style="color: var(--accent-link)"
|
|
55
|
+
>console.anthropic.com</a
|
|
56
|
+
>${" "}— recommended`,
|
|
45
57
|
placeholder: "sk-ant-...",
|
|
46
58
|
},
|
|
47
59
|
{
|
|
@@ -55,7 +67,14 @@ export const kProviderAuthFields = {
|
|
|
55
67
|
{
|
|
56
68
|
key: "OPENAI_API_KEY",
|
|
57
69
|
label: "OpenAI API Key",
|
|
58
|
-
hint: "
|
|
70
|
+
hint: html`From${" "}
|
|
71
|
+
<a
|
|
72
|
+
href="https://platform.openai.com"
|
|
73
|
+
target="_blank"
|
|
74
|
+
class="hover:underline"
|
|
75
|
+
style="color: var(--accent-link)"
|
|
76
|
+
>platform.openai.com</a
|
|
77
|
+
>`,
|
|
59
78
|
placeholder: "sk-...",
|
|
60
79
|
},
|
|
61
80
|
],
|
|
@@ -63,7 +82,14 @@ export const kProviderAuthFields = {
|
|
|
63
82
|
{
|
|
64
83
|
key: "GEMINI_API_KEY",
|
|
65
84
|
label: "Gemini API Key",
|
|
66
|
-
hint: "
|
|
85
|
+
hint: html`From${" "}
|
|
86
|
+
<a
|
|
87
|
+
href="https://aistudio.google.com"
|
|
88
|
+
target="_blank"
|
|
89
|
+
class="hover:underline"
|
|
90
|
+
style="color: var(--accent-link)"
|
|
91
|
+
>aistudio.google.com</a
|
|
92
|
+
>`,
|
|
67
93
|
placeholder: "AI...",
|
|
68
94
|
},
|
|
69
95
|
],
|