@chrysb/alphaclaw 0.4.6-beta.5 → 0.4.6-beta.6
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/lib/public/js/components/envars.js +146 -29
- package/lib/public/js/components/features.js +1 -1
- package/lib/public/js/components/icons.js +52 -0
- package/lib/public/js/components/info-tooltip.js +4 -7
- package/lib/public/js/components/models-tab/provider-auth-card.js +2 -2
- package/lib/public/js/components/models.js +1 -1
- package/lib/public/js/components/providers.js +1 -1
- package/lib/public/js/components/tooltip.js +106 -0
- package/lib/public/js/components/welcome.js +1 -1
- package/lib/public/js/lib/model-config.js +1 -0
- package/lib/server/auth-profiles.js +67 -0
- package/lib/server/constants.js +24 -8
- package/lib/server/doctor/service.js +0 -3
- package/lib/server/gateway.js +28 -29
- package/lib/server/onboarding/index.js +2 -0
- package/lib/server/onboarding/validation.js +2 -2
- package/lib/server/routes/models.js +44 -0
- package/lib/server/routes/onboarding.js +2 -0
- package/lib/server/routes/system.js +42 -1
- package/lib/server.js +5 -0
- package/lib/setup/env.template +1 -0
- package/package.json +1 -1
|
@@ -6,16 +6,44 @@ import { showToast } from "./toast.js";
|
|
|
6
6
|
import { SecretInput } from "./secret-input.js";
|
|
7
7
|
import { PageHeader } from "./page-header.js";
|
|
8
8
|
import { ActionButton } from "./action-button.js";
|
|
9
|
+
import {
|
|
10
|
+
Brain2LineIcon,
|
|
11
|
+
ChatVoiceLineIcon,
|
|
12
|
+
ChevronDownIcon,
|
|
13
|
+
ImageAiLineIcon,
|
|
14
|
+
TextToSpeechLineIcon,
|
|
15
|
+
} from "./icons.js";
|
|
16
|
+
import { Tooltip } from "./tooltip.js";
|
|
9
17
|
const html = htm.bind(h);
|
|
10
18
|
|
|
11
19
|
const kGroupLabels = {
|
|
20
|
+
ai: "AI Provider Keys",
|
|
12
21
|
github: "GitHub",
|
|
13
22
|
channels: "Channels",
|
|
14
23
|
tools: "Tools",
|
|
15
24
|
custom: "Custom",
|
|
16
25
|
};
|
|
17
26
|
|
|
18
|
-
const kGroupOrder = ["github", "channels", "tools", "custom"];
|
|
27
|
+
const kGroupOrder = ["ai", "github", "channels", "tools", "custom"];
|
|
28
|
+
const kDefaultVisibleAiKeys = new Set(["OPENAI_API_KEY", "GEMINI_API_KEY"]);
|
|
29
|
+
const kFeatureIconByName = {
|
|
30
|
+
Embeddings: {
|
|
31
|
+
Icon: Brain2LineIcon,
|
|
32
|
+
label: "Memory embeddings",
|
|
33
|
+
},
|
|
34
|
+
Image: {
|
|
35
|
+
Icon: ImageAiLineIcon,
|
|
36
|
+
label: "Image generation",
|
|
37
|
+
},
|
|
38
|
+
TTS: {
|
|
39
|
+
Icon: TextToSpeechLineIcon,
|
|
40
|
+
label: "Text to speech",
|
|
41
|
+
},
|
|
42
|
+
STT: {
|
|
43
|
+
Icon: ChatVoiceLineIcon,
|
|
44
|
+
label: "Speech to text",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
19
47
|
const normalizeEnvVarKey = (raw) => raw.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
20
48
|
const stripSurroundingQuotes = (raw) => {
|
|
21
49
|
const value = String(raw || "").trim();
|
|
@@ -57,27 +85,80 @@ const kHintByKey = {
|
|
|
57
85
|
ANTHROPIC_TOKEN: html`from <code class="text-xs bg-black/30 px-1 rounded">claude setup-token</code>`,
|
|
58
86
|
OPENAI_API_KEY: html`from <a href="https://platform.openai.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">platform.openai.com</a>`,
|
|
59
87
|
GEMINI_API_KEY: html`from <a href="https://aistudio.google.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">aistudio.google.com</a>`,
|
|
88
|
+
ELEVENLABS_API_KEY: html`from <a href="https://elevenlabs.io" target="_blank" class="hover:underline" style="color: var(--accent-link)">elevenlabs.io</a> · <code class="text-xs bg-black/30 px-1 rounded">XI_API_KEY</code> also supported`,
|
|
60
89
|
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: var(--accent-link)">github settings</a>`,
|
|
61
90
|
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>`,
|
|
62
91
|
TELEGRAM_BOT_TOKEN: html`from <a href="https://t.me/BotFather" target="_blank" class="hover:underline" style="color: var(--accent-link)">@BotFather</a> · <a href="https://docs.openclaw.ai/channels/telegram" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
|
|
63
92
|
DISCORD_BOT_TOKEN: html`from <a href="https://discord.com/developers/applications" target="_blank" class="hover:underline" style="color: var(--accent-link)">developer portal</a> · <a href="https://docs.openclaw.ai/channels/discord" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
|
|
93
|
+
MISTRAL_API_KEY: html`from <a href="https://console.mistral.ai" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.mistral.ai</a>`,
|
|
94
|
+
VOYAGE_API_KEY: html`from <a href="https://dash.voyageai.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">dash.voyageai.com</a>`,
|
|
95
|
+
GROQ_API_KEY: html`from <a href="https://console.groq.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.groq.com</a>`,
|
|
96
|
+
DEEPGRAM_API_KEY: html`from <a href="https://console.deepgram.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.deepgram.com</a>`,
|
|
64
97
|
BRAVE_API_KEY: html`from <a href="https://brave.com/search/api/" target="_blank" class="hover:underline" style="color: var(--accent-link)">brave.com/search/api</a> — free tier available`,
|
|
65
98
|
};
|
|
66
99
|
|
|
67
100
|
const getHintContent = (envVar) => kHintByKey[envVar.key] || envVar.hint || "";
|
|
68
101
|
|
|
102
|
+
const getVisibleFeatureIcons = (envVar) =>
|
|
103
|
+
(Array.isArray(envVar?.features) ? envVar.features : []).filter(
|
|
104
|
+
(feature) => !!kFeatureIconByName[feature],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const splitAiVars = (items) => {
|
|
108
|
+
const visible = [];
|
|
109
|
+
const hidden = [];
|
|
110
|
+
(items || []).forEach((item) => {
|
|
111
|
+
const hasValue = !!String(item?.value || "").trim();
|
|
112
|
+
if (kDefaultVisibleAiKeys.has(item?.key) || hasValue) {
|
|
113
|
+
visible.push(item);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
hidden.push(item);
|
|
117
|
+
});
|
|
118
|
+
return { visible, hidden };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const FeatureIcon = ({ feature }) => {
|
|
122
|
+
const entry = kFeatureIconByName[feature];
|
|
123
|
+
if (!entry) return null;
|
|
124
|
+
const { Icon, label } = entry;
|
|
125
|
+
return html`
|
|
126
|
+
<${Tooltip} text=${label} widthClass="w-auto" tooltipClassName="whitespace-nowrap">
|
|
127
|
+
<span
|
|
128
|
+
class="inline-flex items-center justify-center text-gray-500 hover:text-gray-300 focus-within:text-gray-300"
|
|
129
|
+
tabindex="0"
|
|
130
|
+
aria-label=${label}
|
|
131
|
+
>
|
|
132
|
+
<${Icon} className="w-3.5 h-3.5" />
|
|
133
|
+
</span>
|
|
134
|
+
</${Tooltip}>
|
|
135
|
+
`;
|
|
136
|
+
};
|
|
137
|
+
|
|
69
138
|
const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
70
139
|
const hint = getHintContent(envVar);
|
|
140
|
+
const featureIcons = getVisibleFeatureIcons(envVar);
|
|
71
141
|
|
|
72
142
|
return html`
|
|
73
143
|
<div class="flex items-start gap-4 px-4 py-3">
|
|
74
|
-
<div class="shrink-0
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
144
|
+
<div class="shrink-0" style="width: 200px">
|
|
145
|
+
<div class="flex items-center gap-2 pt-1.5">
|
|
146
|
+
<span
|
|
147
|
+
class="inline-block w-1.5 h-1.5 rounded-full shrink-0 ${envVar.value
|
|
148
|
+
? "bg-green-500"
|
|
149
|
+
: "bg-gray-600"}"
|
|
150
|
+
/>
|
|
151
|
+
<code class="text-sm truncate">${envVar.key}</code>
|
|
152
|
+
</div>
|
|
153
|
+
${featureIcons.length > 0
|
|
154
|
+
? html`
|
|
155
|
+
<div class="flex items-center gap-2 mt-1 pl-3.5">
|
|
156
|
+
${featureIcons.map(
|
|
157
|
+
(feature) => html`<${FeatureIcon} key=${feature} feature=${feature} />`,
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
`
|
|
161
|
+
: null}
|
|
81
162
|
</div>
|
|
82
163
|
<div class="flex-1 min-w-0">
|
|
83
164
|
<div class="flex items-center gap-1">
|
|
@@ -114,6 +195,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
114
195
|
const [secretMaskEpoch, setSecretMaskEpoch] = useState(0);
|
|
115
196
|
const [dirty, setDirty] = useState(false);
|
|
116
197
|
const [saving, setSaving] = useState(false);
|
|
198
|
+
const [showAllAiKeys, setShowAllAiKeys] = useState(false);
|
|
117
199
|
const [newKey, setNewKey] = useState("");
|
|
118
200
|
const baselineSignatureRef = useRef("[]");
|
|
119
201
|
|
|
@@ -310,6 +392,61 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
310
392
|
const pendingAtBottom = grouped.custom.filter((item) => pending.has(item.key));
|
|
311
393
|
grouped.custom = [...nonPending, ...pendingAtBottom];
|
|
312
394
|
}
|
|
395
|
+
const aiSplit = splitAiVars(grouped.ai || []);
|
|
396
|
+
const renderEnvRows = (items) =>
|
|
397
|
+
items.map(
|
|
398
|
+
(v) =>
|
|
399
|
+
html`<${EnvRow}
|
|
400
|
+
key=${`${secretMaskEpoch}:${v.key}`}
|
|
401
|
+
envVar=${v}
|
|
402
|
+
onChange=${handleChange}
|
|
403
|
+
onDelete=${handleDelete}
|
|
404
|
+
disabled=${saving}
|
|
405
|
+
/>`,
|
|
406
|
+
);
|
|
407
|
+
const renderGroupCard = (groupKey) => {
|
|
408
|
+
const items = grouped[groupKey] || [];
|
|
409
|
+
if (!items.length) return null;
|
|
410
|
+
if (groupKey === "ai") {
|
|
411
|
+
const { visible, hidden } = aiSplit;
|
|
412
|
+
const expanded = showAllAiKeys && hidden.length > 0;
|
|
413
|
+
return html`
|
|
414
|
+
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
415
|
+
<h3 class="card-label text-xs px-4 pt-3 pb-2">
|
|
416
|
+
${kGroupLabels[groupKey] || groupKey}
|
|
417
|
+
</h3>
|
|
418
|
+
<div class="divide-y divide-border">${renderEnvRows(visible)}</div>
|
|
419
|
+
${hidden.length > 0
|
|
420
|
+
? html`
|
|
421
|
+
<div class="border-t border-border px-4 py-2">
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
onclick=${() => setShowAllAiKeys((prev) => !prev)}
|
|
425
|
+
class="inline-flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-300"
|
|
426
|
+
>
|
|
427
|
+
<${ChevronDownIcon}
|
|
428
|
+
className=${`transition-transform ${expanded ? "rotate-180" : ""}`}
|
|
429
|
+
/>
|
|
430
|
+
${expanded ? "Show fewer" : `Show more (${hidden.length})`}
|
|
431
|
+
</button>
|
|
432
|
+
</div>
|
|
433
|
+
`
|
|
434
|
+
: null}
|
|
435
|
+
${expanded
|
|
436
|
+
? html`<div class="divide-y divide-border border-t border-border">${renderEnvRows(hidden)}</div>`
|
|
437
|
+
: null}
|
|
438
|
+
</div>
|
|
439
|
+
`;
|
|
440
|
+
}
|
|
441
|
+
return html`
|
|
442
|
+
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
443
|
+
<h3 class="card-label text-xs px-4 pt-3 pb-2">
|
|
444
|
+
${kGroupLabels[groupKey] || groupKey}
|
|
445
|
+
</h3>
|
|
446
|
+
<div class="divide-y divide-border">${renderEnvRows(items)}</div>
|
|
447
|
+
</div>
|
|
448
|
+
`;
|
|
449
|
+
};
|
|
313
450
|
|
|
314
451
|
return html`
|
|
315
452
|
<div class="space-y-4">
|
|
@@ -331,27 +468,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
331
468
|
|
|
332
469
|
${kGroupOrder
|
|
333
470
|
.filter((g) => grouped[g]?.length)
|
|
334
|
-
.map(
|
|
335
|
-
(g) => html`
|
|
336
|
-
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
337
|
-
<h3 class="card-label text-xs px-4 pt-3 pb-2">
|
|
338
|
-
${kGroupLabels[g] || g}
|
|
339
|
-
</h3>
|
|
340
|
-
<div class="divide-y divide-border">
|
|
341
|
-
${grouped[g].map(
|
|
342
|
-
(v) =>
|
|
343
|
-
html`<${EnvRow}
|
|
344
|
-
key=${`${secretMaskEpoch}:${v.key}`}
|
|
345
|
-
envVar=${v}
|
|
346
|
-
onChange=${handleChange}
|
|
347
|
-
onDelete=${handleDelete}
|
|
348
|
-
disabled=${saving}
|
|
349
|
-
/>`,
|
|
350
|
-
)}
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
353
|
-
`,
|
|
354
|
-
)}
|
|
471
|
+
.map((g) => renderGroupCard(g))}
|
|
355
472
|
|
|
356
473
|
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
357
474
|
<div class="flex items-center justify-between px-4 pt-3 pb-2">
|
|
@@ -119,6 +119,58 @@ export const Image2FillIcon = ({ className = "" }) => html`
|
|
|
119
119
|
</svg>
|
|
120
120
|
`;
|
|
121
121
|
|
|
122
|
+
export const ImageAiLineIcon = ({ className = "" }) => html`
|
|
123
|
+
<svg
|
|
124
|
+
class=${className}
|
|
125
|
+
viewBox="0 0 24 24"
|
|
126
|
+
fill="currentColor"
|
|
127
|
+
aria-hidden="true"
|
|
128
|
+
>
|
|
129
|
+
<path
|
|
130
|
+
d="M20.7134 8.12811L20.4668 8.69379C20.2864 9.10792 19.7136 9.10792 19.5331 8.69379L19.2866 8.12811C18.8471 7.11947 18.0555 6.31641 17.0677 5.87708L16.308 5.53922C15.8973 5.35653 15.8973 4.75881 16.308 4.57612L17.0252 4.25714C18.0384 3.80651 18.8442 2.97373 19.2761 1.93083L19.5293 1.31953C19.7058 0.893489 20.2942 0.893489 20.4706 1.31953L20.7238 1.93083C21.1558 2.97373 21.9616 3.80651 22.9748 4.25714L23.6919 4.57612C24.1027 4.75881 24.1027 5.35653 23.6919 5.53922L22.9323 5.87708C21.9445 6.31641 21.1529 7.11947 20.7134 8.12811ZM2.9918 3H14V5H4V19L14 9L20 15V11H22V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934C2 3.44476 2.45531 3 2.9918 3ZM20 17.8284L14 11.8284L6.82843 19H20V17.8284ZM8 11C6.89543 11 6 10.1046 6 9C6 7.89543 6.89543 7 8 7C9.10457 7 10 7.89543 10 9C10 10.1046 9.10457 11 8 11Z"
|
|
131
|
+
/>
|
|
132
|
+
</svg>
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
export const Brain2LineIcon = ({ className = "" }) => html`
|
|
136
|
+
<svg
|
|
137
|
+
class=${className}
|
|
138
|
+
viewBox="0 0 24 24"
|
|
139
|
+
fill="currentColor"
|
|
140
|
+
aria-hidden="true"
|
|
141
|
+
>
|
|
142
|
+
<path
|
|
143
|
+
d="M7 6C7 6.23676 7.04072 6.46184 7.11469 6.66999C7.22686 6.98559 7.17357 7.33638 6.97276 7.60444C6.77194 7.8725 6.45026 8.02222 6.11585 8.00327C6.0776 8.0011 6.03898 8 6 8C4.89543 8 4 8.89543 4 10C4 10.5129 4.19174 10.9786 4.50903 11.3331C4.84885 11.7128 4.84885 12.2872 4.50903 12.6669C4.19174 13.0214 4 13.4871 4 14C4 14.8842 4.57447 15.6369 5.37327 15.9001C5.84924 16.057 6.1356 16.5419 6.04308 17.0345C6.01489 17.1846 6 17.3401 6 17.5C6 18.8807 7.11929 20 8.5 20C9.75862 20 10.8015 19.069 10.9746 17.8583C10.9806 17.8165 10.9891 17.7756 11 17.7358V6C11 4.89543 10.1046 4 9 4C7.89543 4 7 4.89543 7 6ZM13 17.7358C13.0109 17.7756 13.0194 17.8165 13.0254 17.8583C13.1985 19.069 14.2414 20 15.5 20C16.8807 20 18 18.8807 18 17.5C18 17.3401 17.9851 17.1846 17.9569 17.0345C17.8644 16.5419 18.1508 16.057 18.6267 15.9001C19.4255 15.6369 20 14.8842 20 14C20 13.4871 19.8083 13.0214 19.491 12.6669C19.1511 12.2872 19.1511 11.7128 19.491 11.3331C19.8083 10.9786 20 10.5129 20 10C20 8.89543 19.1046 8 18 8C17.961 8 17.9224 8.0011 17.8841 8.00327C17.5497 8.02222 17.2281 7.8725 17.0272 7.60444C16.8264 7.33638 16.7731 6.98559 16.8853 6.66999C16.9593 6.46184 17 6.23676 17 6C17 4.89543 16.1046 4 15 4C13.8954 4 13 4.89543 13 6V17.7358ZM9 2C10.1947 2 11.2671 2.52376 12 3.35418C12.7329 2.52376 13.8053 2 15 2C17.2091 2 19 3.79086 19 6C19 6.04198 18.9994 6.08382 18.9981 6.12552C20.7243 6.56889 22 8.13546 22 10C22 10.728 21.8049 11.4116 21.4646 12C21.8049 12.5884 22 13.272 22 14C22 15.4817 21.1949 16.7734 19.9999 17.4646L20 17.5C20 19.9853 17.9853 22 15.5 22C14.0859 22 12.8248 21.3481 12 20.3285C11.1752 21.3481 9.91405 22 8.5 22C6.01472 22 4 19.9853 4 17.5L4.00014 17.4646C2.80512 16.7734 2 15.4817 2 14C2 13.272 2.19513 12.5884 2.53536 12C2.19513 11.4116 2 10.728 2 10C2 8.13546 3.27573 6.56889 5.00194 6.12552C5.00065 6.08382 5 6.04198 5 6C5 3.79086 6.79086 2 9 2Z"
|
|
144
|
+
/>
|
|
145
|
+
</svg>
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
export const TextToSpeechLineIcon = ({ className = "" }) => html`
|
|
149
|
+
<svg
|
|
150
|
+
class=${className}
|
|
151
|
+
viewBox="0 0 24 24"
|
|
152
|
+
fill="currentColor"
|
|
153
|
+
aria-hidden="true"
|
|
154
|
+
>
|
|
155
|
+
<path
|
|
156
|
+
d="M14.5 5H6C4.89543 5 4 5.89543 4 7V17C4 18.1046 4.89543 19 6 19H18C19.1046 19 20 18.1046 20 17V14.5H22V17C22 19.2091 20.2091 21 18 21H6C3.79086 21 2 19.2091 2 17V7C2 4.79086 3.79086 3 6 3H14.5V5ZM14 11H11V17H9V11H6V9H14V11ZM20.6572 1.34277C22.1049 2.79049 23 4.79086 23 7C23 9.20914 22.1049 11.2095 20.6572 12.6572L19.2422 11.2422C20.328 10.1564 21 8.65685 21 7C21 5.34315 20.328 3.8436 19.2422 2.75781L20.6572 1.34277ZM17.8281 4.17188C18.552 4.89573 19 5.89543 19 7C19 8.10457 18.552 9.10427 17.8281 9.82812L16.4141 8.41406C16.776 8.05213 17 7.55228 17 7C17 6.44772 16.776 5.94787 16.4141 5.58594L17.8281 4.17188Z"
|
|
157
|
+
/>
|
|
158
|
+
</svg>
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
export const ChatVoiceLineIcon = ({ className = "" }) => html`
|
|
162
|
+
<svg
|
|
163
|
+
class=${className}
|
|
164
|
+
viewBox="0 0 24 24"
|
|
165
|
+
fill="currentColor"
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
>
|
|
168
|
+
<path
|
|
169
|
+
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22H2L4.92893 19.0711C3.11929 17.2614 2 14.7614 2 12ZM6.82843 20H12C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 14.1524 4.85124 16.1649 6.34315 17.6569L7.75736 19.0711L6.82843 20ZM11 6H13V18H11V6ZM7 9H9V15H7V9ZM15 9H17V15H15V9Z"
|
|
170
|
+
/>
|
|
171
|
+
</svg>
|
|
172
|
+
`;
|
|
173
|
+
|
|
122
174
|
export const FileMusicLineIcon = ({ className = "" }) => html`
|
|
123
175
|
<svg
|
|
124
176
|
class=${className}
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { Tooltip } from "./tooltip.js";
|
|
3
4
|
|
|
4
5
|
const html = htm.bind(h);
|
|
5
6
|
|
|
6
7
|
export const InfoTooltip = ({ text = "", widthClass = "w-64" }) => html`
|
|
7
|
-
|
|
8
|
+
<${Tooltip} text=${text} widthClass=${widthClass}>
|
|
8
9
|
<span
|
|
9
|
-
class="inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-500 text-[10px] text-gray-400 cursor-default"
|
|
10
|
+
class="inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-500 text-[10px] text-gray-400 cursor-default select-none"
|
|
10
11
|
aria-label=${text}
|
|
11
12
|
>?</span
|
|
12
13
|
>
|
|
13
|
-
|
|
14
|
-
class=${`pointer-events-none absolute left-1/2 top-full z-10 mt-2 hidden -translate-x-1/2 rounded-md border border-border bg-modal px-2 py-1 text-[11px] text-gray-300 shadow-lg group-hover:block ${widthClass}`}
|
|
15
|
-
>${text}</span
|
|
16
|
-
>
|
|
17
|
-
</span>
|
|
14
|
+
</${Tooltip}>
|
|
18
15
|
`;
|
|
@@ -309,14 +309,14 @@ export const ProviderAuthCard = ({
|
|
|
309
309
|
>${mode.label}</label
|
|
310
310
|
>
|
|
311
311
|
${hasMultipleModes && isActive
|
|
312
|
-
? html`<${Badge} tone="cyan">
|
|
312
|
+
? html`<${Badge} tone="cyan">Primary</${Badge}>`
|
|
313
313
|
: null}
|
|
314
314
|
${hasMultipleModes && !isActive && fieldValue
|
|
315
315
|
? html`<button
|
|
316
316
|
onclick=${() => handleSetActive(mode)}
|
|
317
317
|
class="text-xs px-1.5 py-0.5 rounded-full text-gray-500 hover:text-gray-300 hover:bg-white/5"
|
|
318
318
|
>
|
|
319
|
-
Set
|
|
319
|
+
Set primary
|
|
320
320
|
</button>`
|
|
321
321
|
: null}
|
|
322
322
|
${mode.url && !fieldValue
|
|
@@ -268,7 +268,7 @@ export const Models = () => {
|
|
|
268
268
|
: selectedModelProvider === "openai"
|
|
269
269
|
? !!getKeyVal(envVars, "OPENAI_API_KEY")
|
|
270
270
|
: selectedModelProvider === "openai-codex"
|
|
271
|
-
? !!
|
|
271
|
+
? !!codexStatus.connected
|
|
272
272
|
: selectedModelProvider === "google"
|
|
273
273
|
? !!getKeyVal(envVars, "GEMINI_API_KEY")
|
|
274
274
|
: false;
|
|
@@ -232,7 +232,7 @@ export const Providers = ({ onRestartRequired = () => {} }) => {
|
|
|
232
232
|
: selectedModelProvider === "openai"
|
|
233
233
|
? !!getKeyVal(envVars, "OPENAI_API_KEY")
|
|
234
234
|
: selectedModelProvider === "openai-codex"
|
|
235
|
-
? !!
|
|
235
|
+
? !!codexStatus.connected
|
|
236
236
|
: selectedModelProvider === "google"
|
|
237
237
|
? !!getKeyVal(envVars, "GEMINI_API_KEY")
|
|
238
238
|
: false;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import { createPortal } from "https://esm.sh/preact/compat";
|
|
4
|
+
import htm from "https://esm.sh/htm";
|
|
5
|
+
|
|
6
|
+
const html = htm.bind(h);
|
|
7
|
+
|
|
8
|
+
const kViewportPadding = 8;
|
|
9
|
+
const kTooltipOffset = 8;
|
|
10
|
+
|
|
11
|
+
const getTooltipPosition = (triggerEl, tooltipEl) => {
|
|
12
|
+
if (!triggerEl) return null;
|
|
13
|
+
const triggerRect = triggerEl.getBoundingClientRect();
|
|
14
|
+
const tooltipRect = tooltipEl?.getBoundingClientRect?.() || {
|
|
15
|
+
width: 0,
|
|
16
|
+
height: 0,
|
|
17
|
+
};
|
|
18
|
+
const minLeft = kViewportPadding + tooltipRect.width / 2;
|
|
19
|
+
const maxLeft = window.innerWidth - kViewportPadding - tooltipRect.width / 2;
|
|
20
|
+
const centeredLeft = triggerRect.left + triggerRect.width / 2;
|
|
21
|
+
const left = tooltipRect.width
|
|
22
|
+
? Math.min(Math.max(centeredLeft, minLeft), maxLeft)
|
|
23
|
+
: centeredLeft;
|
|
24
|
+
|
|
25
|
+
let top = triggerRect.bottom + kTooltipOffset;
|
|
26
|
+
const canRenderAbove =
|
|
27
|
+
triggerRect.top - kTooltipOffset - tooltipRect.height >= kViewportPadding;
|
|
28
|
+
const wouldOverflowBelow =
|
|
29
|
+
top + tooltipRect.height + kViewportPadding > window.innerHeight;
|
|
30
|
+
if (wouldOverflowBelow && canRenderAbove) {
|
|
31
|
+
top = triggerRect.top - kTooltipOffset - tooltipRect.height;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
left: `${left}px`,
|
|
36
|
+
top: `${Math.max(kViewportPadding, top)}px`,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Tooltip = ({
|
|
41
|
+
text = "",
|
|
42
|
+
widthClass = "w-64",
|
|
43
|
+
tooltipClassName = "",
|
|
44
|
+
children = null,
|
|
45
|
+
disabled = false,
|
|
46
|
+
}) => {
|
|
47
|
+
const triggerRef = useRef(null);
|
|
48
|
+
const tooltipRef = useRef(null);
|
|
49
|
+
const [open, setOpen] = useState(false);
|
|
50
|
+
const [positionStyle, setPositionStyle] = useState(null);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!open || disabled || !text) return undefined;
|
|
54
|
+
|
|
55
|
+
const updatePosition = () => {
|
|
56
|
+
const nextStyle = getTooltipPosition(triggerRef.current, tooltipRef.current);
|
|
57
|
+
if (nextStyle) setPositionStyle(nextStyle);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
updatePosition();
|
|
61
|
+
window.addEventListener("resize", updatePosition);
|
|
62
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
63
|
+
return () => {
|
|
64
|
+
window.removeEventListener("resize", updatePosition);
|
|
65
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
66
|
+
};
|
|
67
|
+
}, [open, disabled, text]);
|
|
68
|
+
|
|
69
|
+
const handleOpen = () => {
|
|
70
|
+
if (disabled || !text) return;
|
|
71
|
+
setOpen(true);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleClose = () => setOpen(false);
|
|
75
|
+
|
|
76
|
+
return html`
|
|
77
|
+
<span
|
|
78
|
+
ref=${triggerRef}
|
|
79
|
+
class="inline-flex"
|
|
80
|
+
onMouseEnter=${handleOpen}
|
|
81
|
+
onMouseLeave=${handleClose}
|
|
82
|
+
onFocusIn=${handleOpen}
|
|
83
|
+
onFocusOut=${(event) => {
|
|
84
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
85
|
+
handleClose();
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
${children}
|
|
89
|
+
${open && !disabled && text && typeof document !== "undefined"
|
|
90
|
+
? createPortal(
|
|
91
|
+
html`
|
|
92
|
+
<span
|
|
93
|
+
ref=${tooltipRef}
|
|
94
|
+
role="tooltip"
|
|
95
|
+
class=${`pointer-events-none fixed left-0 top-0 z-[80] -translate-x-1/2 rounded-md border border-border bg-modal px-2 py-1 text-[11px] text-gray-300 shadow-lg ${widthClass} ${tooltipClassName}`.trim()}
|
|
96
|
+
style=${positionStyle || { visibility: "hidden" }}
|
|
97
|
+
>
|
|
98
|
+
${text}
|
|
99
|
+
</span>
|
|
100
|
+
`,
|
|
101
|
+
document.body,
|
|
102
|
+
)
|
|
103
|
+
: null}
|
|
104
|
+
</span>
|
|
105
|
+
`;
|
|
106
|
+
};
|
|
@@ -101,7 +101,7 @@ export const Welcome = ({ onComplete }) => {
|
|
|
101
101
|
: selectedProvider === "google"
|
|
102
102
|
? !!vals.GEMINI_API_KEY
|
|
103
103
|
: selectedProvider === "openai-codex"
|
|
104
|
-
? !!
|
|
104
|
+
? !!codexStatus.connected
|
|
105
105
|
: false;
|
|
106
106
|
|
|
107
107
|
const allValid = kWelcomeGroups.every((g) => g.validate(vals, { hasAi }));
|
|
@@ -158,6 +158,7 @@ export const kFeatureDefs = [
|
|
|
158
158
|
];
|
|
159
159
|
|
|
160
160
|
export const getVisibleAiFieldKeys = (provider) => {
|
|
161
|
+
if (provider === "openai-codex") return new Set();
|
|
161
162
|
const authProvider = getAuthProviderFromModelProvider(provider);
|
|
162
163
|
const fields = kProviderAuthFields[authProvider] || [];
|
|
163
164
|
return new Set(fields.map((field) => field.key));
|
|
@@ -3,6 +3,15 @@ const path = require("path");
|
|
|
3
3
|
const { AUTH_PROFILES_PATH, CODEX_PROFILE_ID, OPENCLAW_DIR } = require("./constants");
|
|
4
4
|
|
|
5
5
|
const kDefaultAgentId = "main";
|
|
6
|
+
const kApiKeyEnvVarByProvider = {
|
|
7
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
8
|
+
openai: "OPENAI_API_KEY",
|
|
9
|
+
google: "GEMINI_API_KEY",
|
|
10
|
+
mistral: "MISTRAL_API_KEY",
|
|
11
|
+
voyage: "VOYAGE_API_KEY",
|
|
12
|
+
groq: "GROQ_API_KEY",
|
|
13
|
+
deepgram: "DEEPGRAM_API_KEY",
|
|
14
|
+
};
|
|
6
15
|
|
|
7
16
|
const normalizeSecret = (raw) =>
|
|
8
17
|
String(raw ?? "")
|
|
@@ -15,6 +24,14 @@ const credentialMode = (credential) => {
|
|
|
15
24
|
return "oauth";
|
|
16
25
|
};
|
|
17
26
|
|
|
27
|
+
const getEnvVarForApiKeyProvider = (provider) =>
|
|
28
|
+
kApiKeyEnvVarByProvider[String(provider || "").trim()] || "";
|
|
29
|
+
|
|
30
|
+
const getDefaultProfileIdForApiKeyProvider = (provider) => {
|
|
31
|
+
const normalized = String(provider || "").trim();
|
|
32
|
+
return normalized ? `${normalized}:default` : "";
|
|
33
|
+
};
|
|
34
|
+
|
|
18
35
|
const resolveAgentDir = (agentId = kDefaultAgentId) =>
|
|
19
36
|
path.join(OPENCLAW_DIR, "agents", agentId, "agent");
|
|
20
37
|
|
|
@@ -24,6 +41,9 @@ const resolveAuthProfilesPath = (agentId = kDefaultAgentId) =>
|
|
|
24
41
|
const resolveOpenclawConfigPath = () =>
|
|
25
42
|
path.join(OPENCLAW_DIR, "openclaw.json");
|
|
26
43
|
|
|
44
|
+
const hasCompletedOnboardingConfig = (cfg) =>
|
|
45
|
+
String(cfg?.agents?.defaults?.model?.primary || "").trim().includes("/");
|
|
46
|
+
|
|
27
47
|
const loadAuthStore = (agentId = kDefaultAgentId) => {
|
|
28
48
|
const storePath = resolveAuthProfilesPath(agentId);
|
|
29
49
|
let store = { version: 1, profiles: {} };
|
|
@@ -79,6 +99,17 @@ const loadOpenclawConfig = () => {
|
|
|
79
99
|
}
|
|
80
100
|
};
|
|
81
101
|
|
|
102
|
+
const canSyncOpenclawAuthReferences = () => {
|
|
103
|
+
const configPath = resolveOpenclawConfigPath();
|
|
104
|
+
if (!fs.existsSync(configPath)) return false;
|
|
105
|
+
try {
|
|
106
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
107
|
+
return hasCompletedOnboardingConfig(cfg);
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
82
113
|
const saveOpenclawConfig = (cfg) => {
|
|
83
114
|
const configPath = resolveOpenclawConfigPath();
|
|
84
115
|
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
@@ -142,6 +173,7 @@ const createAuthProfiles = () => {
|
|
|
142
173
|
store.profiles[profileId] = sanitized;
|
|
143
174
|
saveAuthStore(agentId, store);
|
|
144
175
|
|
|
176
|
+
if (!canSyncOpenclawAuthReferences()) return;
|
|
145
177
|
const cfg = loadOpenclawConfig();
|
|
146
178
|
const updated = syncConfigAuthReference(cfg, profileId, sanitized);
|
|
147
179
|
saveOpenclawConfig(updated);
|
|
@@ -153,6 +185,7 @@ const createAuthProfiles = () => {
|
|
|
153
185
|
delete store.profiles[profileId];
|
|
154
186
|
saveAuthStore(agentId, store);
|
|
155
187
|
|
|
188
|
+
if (!canSyncOpenclawAuthReferences()) return true;
|
|
156
189
|
const cfg = loadOpenclawConfig();
|
|
157
190
|
const updated = removeConfigAuthReference(cfg, profileId);
|
|
158
191
|
saveOpenclawConfig(updated);
|
|
@@ -167,6 +200,7 @@ const createAuthProfiles = () => {
|
|
|
167
200
|
};
|
|
168
201
|
|
|
169
202
|
const syncConfigAuthReferencesForAgent = (agentId = kDefaultAgentId) => {
|
|
203
|
+
if (!canSyncOpenclawAuthReferences()) return;
|
|
170
204
|
const store = loadAuthStore(agentId);
|
|
171
205
|
let cfg = loadOpenclawConfig();
|
|
172
206
|
for (const [profileId, credential] of Object.entries(store.profiles || {})) {
|
|
@@ -176,6 +210,34 @@ const createAuthProfiles = () => {
|
|
|
176
210
|
saveOpenclawConfig(cfg);
|
|
177
211
|
};
|
|
178
212
|
|
|
213
|
+
const upsertApiKeyProfileForEnvVar = (
|
|
214
|
+
provider,
|
|
215
|
+
rawValue,
|
|
216
|
+
agentId = kDefaultAgentId,
|
|
217
|
+
) => {
|
|
218
|
+
const key = normalizeSecret(rawValue);
|
|
219
|
+
if (!provider || !key) return false;
|
|
220
|
+
upsertProfile(
|
|
221
|
+
getDefaultProfileIdForApiKeyProvider(provider),
|
|
222
|
+
{
|
|
223
|
+
type: "api_key",
|
|
224
|
+
provider,
|
|
225
|
+
key,
|
|
226
|
+
},
|
|
227
|
+
agentId,
|
|
228
|
+
);
|
|
229
|
+
return true;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const removeApiKeyProfileForEnvVar = (provider, agentId = kDefaultAgentId) => {
|
|
233
|
+
const profileId = getDefaultProfileIdForApiKeyProvider(provider);
|
|
234
|
+
if (!profileId) return false;
|
|
235
|
+
const existing = getProfile(profileId, agentId);
|
|
236
|
+
if (!existing) return false;
|
|
237
|
+
if (existing.type !== "api_key" || existing.provider !== provider) return false;
|
|
238
|
+
return removeProfile(profileId, agentId);
|
|
239
|
+
};
|
|
240
|
+
|
|
179
241
|
// ── Model config operations ──
|
|
180
242
|
|
|
181
243
|
const getModelConfig = () => {
|
|
@@ -240,6 +302,7 @@ const createAuthProfiles = () => {
|
|
|
240
302
|
}
|
|
241
303
|
if (changed) {
|
|
242
304
|
saveAuthStore(kDefaultAgentId, store);
|
|
305
|
+
if (!canSyncOpenclawAuthReferences()) return changed;
|
|
243
306
|
let cfg = loadOpenclawConfig();
|
|
244
307
|
for (const [id, cred] of Object.entries(cfg.auth?.profiles || {})) {
|
|
245
308
|
if (cred?.provider === "openai-codex") {
|
|
@@ -259,6 +322,10 @@ const createAuthProfiles = () => {
|
|
|
259
322
|
removeProfile,
|
|
260
323
|
setAuthOrder,
|
|
261
324
|
syncConfigAuthReferencesForAgent,
|
|
325
|
+
upsertApiKeyProfileForEnvVar,
|
|
326
|
+
removeApiKeyProfileForEnvVar,
|
|
327
|
+
getEnvVarForApiKeyProvider,
|
|
328
|
+
getDefaultProfileIdForApiKeyProvider,
|
|
262
329
|
getModelConfig,
|
|
263
330
|
setModelConfig,
|
|
264
331
|
getCodexProfile,
|
package/lib/server/constants.js
CHANGED
|
@@ -153,26 +153,38 @@ const kKnownVars = [
|
|
|
153
153
|
{
|
|
154
154
|
key: "ANTHROPIC_API_KEY",
|
|
155
155
|
label: "Anthropic API Key",
|
|
156
|
-
group: "
|
|
156
|
+
group: "ai",
|
|
157
157
|
hint: "From console.anthropic.com",
|
|
158
|
+
features: ["Models"],
|
|
158
159
|
},
|
|
159
160
|
{
|
|
160
161
|
key: "ANTHROPIC_TOKEN",
|
|
161
162
|
label: "Anthropic Setup Token",
|
|
162
|
-
group: "
|
|
163
|
+
group: "ai",
|
|
163
164
|
hint: "From claude setup-token",
|
|
165
|
+
features: ["Models"],
|
|
166
|
+
visibleInEnvars: false,
|
|
164
167
|
},
|
|
165
168
|
{
|
|
166
169
|
key: "OPENAI_API_KEY",
|
|
167
170
|
label: "OpenAI API Key",
|
|
168
|
-
group: "
|
|
171
|
+
group: "ai",
|
|
169
172
|
hint: "From platform.openai.com",
|
|
173
|
+
features: ["Models", "Embeddings", "TTS", "STT"],
|
|
170
174
|
},
|
|
171
175
|
{
|
|
172
176
|
key: "GEMINI_API_KEY",
|
|
173
177
|
label: "Gemini API Key",
|
|
174
|
-
group: "
|
|
178
|
+
group: "ai",
|
|
175
179
|
hint: "From aistudio.google.com",
|
|
180
|
+
features: ["Models", "Embeddings", "Image", "STT"],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
key: "ELEVENLABS_API_KEY",
|
|
184
|
+
label: "ElevenLabs API Key",
|
|
185
|
+
group: "ai",
|
|
186
|
+
hint: "From elevenlabs.io (XI_API_KEY also works)",
|
|
187
|
+
features: ["TTS"],
|
|
176
188
|
},
|
|
177
189
|
{
|
|
178
190
|
key: "GITHUB_TOKEN",
|
|
@@ -201,26 +213,30 @@ const kKnownVars = [
|
|
|
201
213
|
{
|
|
202
214
|
key: "MISTRAL_API_KEY",
|
|
203
215
|
label: "Mistral API Key",
|
|
204
|
-
group: "
|
|
216
|
+
group: "ai",
|
|
205
217
|
hint: "From console.mistral.ai",
|
|
218
|
+
features: ["Models", "Embeddings", "STT"],
|
|
206
219
|
},
|
|
207
220
|
{
|
|
208
221
|
key: "VOYAGE_API_KEY",
|
|
209
222
|
label: "Voyage API Key",
|
|
210
|
-
group: "
|
|
223
|
+
group: "ai",
|
|
211
224
|
hint: "From dash.voyageai.com",
|
|
225
|
+
features: ["Embeddings"],
|
|
212
226
|
},
|
|
213
227
|
{
|
|
214
228
|
key: "GROQ_API_KEY",
|
|
215
229
|
label: "Groq API Key",
|
|
216
|
-
group: "
|
|
230
|
+
group: "ai",
|
|
217
231
|
hint: "From console.groq.com",
|
|
232
|
+
features: ["Models", "STT"],
|
|
218
233
|
},
|
|
219
234
|
{
|
|
220
235
|
key: "DEEPGRAM_API_KEY",
|
|
221
236
|
label: "Deepgram API Key",
|
|
222
|
-
group: "
|
|
237
|
+
group: "ai",
|
|
223
238
|
hint: "From console.deepgram.com",
|
|
239
|
+
features: ["STT"],
|
|
224
240
|
},
|
|
225
241
|
{
|
|
226
242
|
key: "BRAVE_API_KEY",
|
|
@@ -230,9 +230,6 @@ const createDoctorService = ({
|
|
|
230
230
|
}
|
|
231
231
|
const stdoutText = String(result.stdout || "");
|
|
232
232
|
const stderrText = String(result.stderr || "");
|
|
233
|
-
console.log(
|
|
234
|
-
`[doctor] run ${runId} command result ok=${result.ok} code=${result.code ?? 0} stdout_chars=${stdoutText.length} stderr_chars=${stderrText.length}`,
|
|
235
|
-
);
|
|
236
233
|
let normalizedResult = null;
|
|
237
234
|
try {
|
|
238
235
|
normalizedResult = normalizeDoctorResult(stdoutText);
|
package/lib/server/gateway.js
CHANGED
|
@@ -45,7 +45,19 @@ const gatewayEnv = () => ({
|
|
|
45
45
|
XDG_CONFIG_HOME: OPENCLAW_DIR,
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
const isOnboarded = () =>
|
|
48
|
+
const isOnboarded = () => {
|
|
49
|
+
const configPath = `${OPENCLAW_DIR}/openclaw.json`;
|
|
50
|
+
if (!fs.existsSync(configPath)) return false;
|
|
51
|
+
try {
|
|
52
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
53
|
+
const primaryModel = String(
|
|
54
|
+
config?.agents?.defaults?.model?.primary || "",
|
|
55
|
+
).trim();
|
|
56
|
+
return primaryModel.includes("/");
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
49
61
|
|
|
50
62
|
const isGatewayRunning = () =>
|
|
51
63
|
new Promise((resolve) => {
|
|
@@ -133,6 +145,19 @@ const launchGatewayProcess = () => {
|
|
|
133
145
|
return child;
|
|
134
146
|
};
|
|
135
147
|
|
|
148
|
+
const markManagedGatewayExitExpected = () => {
|
|
149
|
+
if (
|
|
150
|
+
!gatewayChild ||
|
|
151
|
+
gatewayChild.exitCode !== null ||
|
|
152
|
+
gatewayChild.killed ||
|
|
153
|
+
!gatewayChild.pid
|
|
154
|
+
) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
expectedExitPids.add(gatewayChild.pid);
|
|
158
|
+
return true;
|
|
159
|
+
};
|
|
160
|
+
|
|
136
161
|
const startGateway = async () => {
|
|
137
162
|
if (!isOnboarded()) {
|
|
138
163
|
console.log("[alphaclaw] Not onboarded yet — skipping gateway start");
|
|
@@ -148,34 +173,8 @@ const startGateway = async () => {
|
|
|
148
173
|
|
|
149
174
|
const restartGateway = (reloadEnv) => {
|
|
150
175
|
reloadEnv();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
expectedExitPids.add(gatewayChild.pid);
|
|
155
|
-
gatewayChild.kill("SIGTERM");
|
|
156
|
-
gatewayChild = null;
|
|
157
|
-
} catch (e) {
|
|
158
|
-
console.log(
|
|
159
|
-
`[alphaclaw] Failed to stop managed gateway process: ${e.message}`,
|
|
160
|
-
);
|
|
161
|
-
runGatewayCmd("stop");
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
runGatewayCmd("stop");
|
|
165
|
-
}
|
|
166
|
-
runGatewayCmd("start");
|
|
167
|
-
const launchWhenReady = async () => {
|
|
168
|
-
const waitUntil = Date.now() + 8000;
|
|
169
|
-
while (Date.now() < waitUntil) {
|
|
170
|
-
if (!(await isGatewayRunning())) break;
|
|
171
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
172
|
-
}
|
|
173
|
-
console.log(
|
|
174
|
-
"[alphaclaw] Starting openclaw gateway with refreshed environment...",
|
|
175
|
-
);
|
|
176
|
-
launchGatewayProcess();
|
|
177
|
-
};
|
|
178
|
-
void launchWhenReady();
|
|
176
|
+
markManagedGatewayExitExpected();
|
|
177
|
+
runGatewayCmd("--force");
|
|
179
178
|
};
|
|
180
179
|
|
|
181
180
|
const attachGatewaySignalHandlers = () => {
|
|
@@ -29,6 +29,7 @@ const createOnboardingService = ({
|
|
|
29
29
|
resolveGithubRepoUrl,
|
|
30
30
|
resolveModelProvider,
|
|
31
31
|
hasCodexOauthProfile,
|
|
32
|
+
authProfiles,
|
|
32
33
|
ensureGatewayProxyConfig,
|
|
33
34
|
getBaseUrl,
|
|
34
35
|
startGateway,
|
|
@@ -148,6 +149,7 @@ const createOnboardingService = ({
|
|
|
148
149
|
} catch {}
|
|
149
150
|
|
|
150
151
|
writeSanitizedOpenclawConfig({ fs, openclawDir: OPENCLAW_DIR, varMap });
|
|
152
|
+
authProfiles?.syncConfigAuthReferencesForAgent?.();
|
|
151
153
|
ensureGatewayProxyConfig(getBaseUrl(req));
|
|
152
154
|
|
|
153
155
|
installControlUiSkill({
|
|
@@ -46,7 +46,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
|
|
|
46
46
|
const hasAiByProvider = {
|
|
47
47
|
anthropic: !!(varMap.ANTHROPIC_API_KEY || varMap.ANTHROPIC_TOKEN),
|
|
48
48
|
openai: !!varMap.OPENAI_API_KEY,
|
|
49
|
-
"openai-codex": !!
|
|
49
|
+
"openai-codex": !!hasCodexOauth,
|
|
50
50
|
google: !!varMap.GEMINI_API_KEY,
|
|
51
51
|
};
|
|
52
52
|
const hasAnyAi = !!(
|
|
@@ -68,7 +68,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
|
|
|
68
68
|
return {
|
|
69
69
|
ok: false,
|
|
70
70
|
status: 400,
|
|
71
|
-
error: "Connect OpenAI Codex OAuth
|
|
71
|
+
error: "Connect OpenAI Codex OAuth before continuing",
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
return {
|
|
@@ -19,7 +19,49 @@ const registerModelRoutes = ({
|
|
|
19
19
|
parseJsonFromNoisyOutput,
|
|
20
20
|
normalizeOnboardingModels,
|
|
21
21
|
authProfiles,
|
|
22
|
+
readEnvFile,
|
|
23
|
+
writeEnvFile,
|
|
24
|
+
reloadEnv,
|
|
22
25
|
}) => {
|
|
26
|
+
const upsertEnvVar = (items, key, value) => {
|
|
27
|
+
const next = Array.isArray(items) ? [...items] : [];
|
|
28
|
+
const existing = next.find((entry) => entry.key === key);
|
|
29
|
+
if (existing) {
|
|
30
|
+
existing.value = value;
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
next.push({ key, value });
|
|
34
|
+
return next;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const syncEnvVarsForProfiles = (profiles) => {
|
|
38
|
+
if (
|
|
39
|
+
!Array.isArray(profiles) ||
|
|
40
|
+
typeof readEnvFile !== "function" ||
|
|
41
|
+
typeof writeEnvFile !== "function" ||
|
|
42
|
+
typeof reloadEnv !== "function"
|
|
43
|
+
) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let nextEnvVars = readEnvFile();
|
|
47
|
+
let changed = false;
|
|
48
|
+
for (const profile of profiles) {
|
|
49
|
+
if (profile?.type !== "api_key") continue;
|
|
50
|
+
const envKey = authProfiles.getEnvVarForApiKeyProvider?.(profile.provider);
|
|
51
|
+
const envValue = String(profile?.key || "").trim();
|
|
52
|
+
if (!envKey || !envValue) continue;
|
|
53
|
+
const prevValue = String(
|
|
54
|
+
nextEnvVars.find((entry) => entry.key === envKey)?.value || "",
|
|
55
|
+
);
|
|
56
|
+
if (prevValue === envValue) continue;
|
|
57
|
+
nextEnvVars = upsertEnvVar(nextEnvVars, envKey, envValue);
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
60
|
+
if (!changed) return;
|
|
61
|
+
writeEnvFile(nextEnvVars);
|
|
62
|
+
reloadEnv();
|
|
63
|
+
};
|
|
64
|
+
|
|
23
65
|
// ── Existing CLI-backed catalog/status routes ──
|
|
24
66
|
|
|
25
67
|
app.get("/api/models", async (req, res) => {
|
|
@@ -134,6 +176,7 @@ const registerModelRoutes = ({
|
|
|
134
176
|
authProfiles.upsertProfile(profileId, credential, agentId);
|
|
135
177
|
}
|
|
136
178
|
}
|
|
179
|
+
syncEnvVarsForProfiles(profiles);
|
|
137
180
|
}
|
|
138
181
|
|
|
139
182
|
if (authOrder && typeof authOrder === "object") {
|
|
@@ -200,6 +243,7 @@ const registerModelRoutes = ({
|
|
|
200
243
|
try {
|
|
201
244
|
const agentId = req.query.agentId || undefined;
|
|
202
245
|
authProfiles.upsertProfile(profileId, credential, agentId);
|
|
246
|
+
syncEnvVarsForProfiles([{ id: profileId, ...credential }]);
|
|
203
247
|
res.json({ ok: true });
|
|
204
248
|
} catch (err) {
|
|
205
249
|
res
|
|
@@ -71,6 +71,7 @@ const registerOnboardingRoutes = ({
|
|
|
71
71
|
resolveGithubRepoUrl,
|
|
72
72
|
resolveModelProvider,
|
|
73
73
|
hasCodexOauthProfile,
|
|
74
|
+
authProfiles,
|
|
74
75
|
ensureGatewayProxyConfig,
|
|
75
76
|
getBaseUrl,
|
|
76
77
|
startGateway,
|
|
@@ -85,6 +86,7 @@ const registerOnboardingRoutes = ({
|
|
|
85
86
|
resolveGithubRepoUrl,
|
|
86
87
|
resolveModelProvider,
|
|
87
88
|
hasCodexOauthProfile,
|
|
89
|
+
authProfiles,
|
|
88
90
|
ensureGatewayProxyConfig,
|
|
89
91
|
getBaseUrl,
|
|
90
92
|
startGateway,
|
|
@@ -21,6 +21,7 @@ const registerSystemRoutes = ({
|
|
|
21
21
|
OPENCLAW_DIR,
|
|
22
22
|
restartRequiredState,
|
|
23
23
|
topicRegistry,
|
|
24
|
+
authProfiles,
|
|
24
25
|
}) => {
|
|
25
26
|
let envRestartPending = false;
|
|
26
27
|
const kEnvVarsReservedForUserInput = new Set([
|
|
@@ -93,6 +94,34 @@ const registerSystemRoutes = ({
|
|
|
93
94
|
}
|
|
94
95
|
return key || "Session";
|
|
95
96
|
};
|
|
97
|
+
const syncApiKeyAuthProfilesFromEnvVars = (nextEnvVars) => {
|
|
98
|
+
if (!authProfiles) return;
|
|
99
|
+
const envMap = new Map(
|
|
100
|
+
(nextEnvVars || []).map((entry) => [
|
|
101
|
+
String(entry?.key || "").trim(),
|
|
102
|
+
String(entry?.value || ""),
|
|
103
|
+
]),
|
|
104
|
+
);
|
|
105
|
+
const providers = [
|
|
106
|
+
"anthropic",
|
|
107
|
+
"openai",
|
|
108
|
+
"google",
|
|
109
|
+
"mistral",
|
|
110
|
+
"voyage",
|
|
111
|
+
"groq",
|
|
112
|
+
"deepgram",
|
|
113
|
+
];
|
|
114
|
+
for (const provider of providers) {
|
|
115
|
+
const envKey = authProfiles.getEnvVarForApiKeyProvider?.(provider);
|
|
116
|
+
if (!envKey) continue;
|
|
117
|
+
const value = envMap.get(envKey) || "";
|
|
118
|
+
if (!value.trim()) {
|
|
119
|
+
authProfiles.removeApiKeyProfileForEnvVar?.(provider);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
authProfiles.upsertApiKeyProfileForEnvVar(provider, value);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
96
125
|
const listSendableAgentSessions = async () => {
|
|
97
126
|
const result = await clawCmd("sessions --json", { quiet: true });
|
|
98
127
|
if (!result.ok) {
|
|
@@ -168,6 +197,7 @@ const registerSystemRoutes = ({
|
|
|
168
197
|
}
|
|
169
198
|
return getSystemCronStatus();
|
|
170
199
|
};
|
|
200
|
+
const isVisibleInEnvars = (def) => def?.visibleInEnvars !== false;
|
|
171
201
|
|
|
172
202
|
app.get("/api/env", (req, res) => {
|
|
173
203
|
const fileVars = readEnvFile();
|
|
@@ -175,6 +205,7 @@ const registerSystemRoutes = ({
|
|
|
175
205
|
|
|
176
206
|
for (const def of kKnownVars) {
|
|
177
207
|
if (isReservedUserEnvVar(def.key)) continue;
|
|
208
|
+
if (!isVisibleInEnvars(def)) continue;
|
|
178
209
|
const fileEntry = fileVars.find((v) => v.key === def.key);
|
|
179
210
|
const value = fileEntry?.value || "";
|
|
180
211
|
merged.push({
|
|
@@ -183,6 +214,7 @@ const registerSystemRoutes = ({
|
|
|
183
214
|
label: def.label,
|
|
184
215
|
group: def.group,
|
|
185
216
|
hint: def.hint,
|
|
217
|
+
features: def.features,
|
|
186
218
|
source: fileEntry?.value ? "env_file" : "unset",
|
|
187
219
|
editable: true,
|
|
188
220
|
});
|
|
@@ -232,10 +264,19 @@ const registerSystemRoutes = ({
|
|
|
232
264
|
const existingLockedVars = readEnvFile().filter((v) =>
|
|
233
265
|
isReservedUserEnvVar(v.key),
|
|
234
266
|
);
|
|
235
|
-
const
|
|
267
|
+
const hiddenKnownVarKeys = new Set(
|
|
268
|
+
kKnownVars
|
|
269
|
+
.filter((def) => !isReservedUserEnvVar(def.key) && !isVisibleInEnvars(def))
|
|
270
|
+
.map((def) => def.key),
|
|
271
|
+
);
|
|
272
|
+
const existingHiddenKnownVars = readEnvFile().filter((v) =>
|
|
273
|
+
hiddenKnownVarKeys.has(v.key),
|
|
274
|
+
);
|
|
275
|
+
const nextEnvVars = [...filtered, ...existingHiddenKnownVars, ...existingLockedVars];
|
|
236
276
|
syncChannelConfig(nextEnvVars, "remove");
|
|
237
277
|
writeEnvFile(nextEnvVars);
|
|
238
278
|
const changed = reloadEnv();
|
|
279
|
+
syncApiKeyAuthProfilesFromEnvVars(nextEnvVars);
|
|
239
280
|
if (changed && isOnboarded()) {
|
|
240
281
|
envRestartPending = true;
|
|
241
282
|
}
|
package/lib/server.js
CHANGED
|
@@ -204,6 +204,9 @@ registerModelRoutes({
|
|
|
204
204
|
parseJsonFromNoisyOutput,
|
|
205
205
|
normalizeOnboardingModels,
|
|
206
206
|
authProfiles,
|
|
207
|
+
readEnvFile,
|
|
208
|
+
writeEnvFile,
|
|
209
|
+
reloadEnv,
|
|
207
210
|
});
|
|
208
211
|
registerOnboardingRoutes({
|
|
209
212
|
app,
|
|
@@ -217,6 +220,7 @@ registerOnboardingRoutes({
|
|
|
217
220
|
resolveGithubRepoUrl,
|
|
218
221
|
resolveModelProvider,
|
|
219
222
|
hasCodexOauthProfile: authProfiles.hasCodexOauthProfile,
|
|
223
|
+
authProfiles,
|
|
220
224
|
ensureGatewayProxyConfig,
|
|
221
225
|
getBaseUrl,
|
|
222
226
|
startGateway,
|
|
@@ -242,6 +246,7 @@ registerSystemRoutes({
|
|
|
242
246
|
OPENCLAW_DIR: constants.OPENCLAW_DIR,
|
|
243
247
|
restartRequiredState,
|
|
244
248
|
topicRegistry,
|
|
249
|
+
authProfiles,
|
|
245
250
|
});
|
|
246
251
|
registerBrowseRoutes({
|
|
247
252
|
app,
|
package/lib/setup/env.template
CHANGED