@chrysb/alphaclaw 0.4.6-beta.5 → 0.4.6-beta.7

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.
@@ -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
- <span class="relative group inline-flex items-center cursor-default select-none">
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
- <span
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">Active</${Badge}>`
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 active
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
- ? !!(codexStatus.connected || getKeyVal(envVars, "OPENAI_API_KEY"))
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
- ? !!(codexStatus.connected || getKeyVal(envVars, "OPENAI_API_KEY"))
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
+ };
@@ -6,8 +6,10 @@ import {
6
6
  createWebhook,
7
7
  deleteWebhook,
8
8
  fetchWebhookDetail,
9
+ fetchWebhookRequest,
9
10
  fetchWebhookRequests,
10
11
  fetchWebhooks,
12
+ sendAgentMessage,
11
13
  } from "../lib/api.js";
12
14
  import {
13
15
  formatLocaleDateTime,
@@ -17,6 +19,7 @@ import { showToast } from "./toast.js";
17
19
  import { PageHeader } from "./page-header.js";
18
20
  import { ConfirmDialog } from "./confirm-dialog.js";
19
21
  import { ActionButton } from "./action-button.js";
22
+ import { AgentSendModal } from "./agent-send-modal.js";
20
23
  import { ModalShell } from "./modal-shell.js";
21
24
  import { Badge } from "./badge.js";
22
25
  import { CloseIcon } from "./icons.js";
@@ -82,6 +85,40 @@ const jsonPretty = (value) => {
82
85
  }
83
86
  };
84
87
 
88
+ const buildWebhookDebugMessage = ({
89
+ hookName = "",
90
+ webhook = null,
91
+ request = null,
92
+ }) => {
93
+ const hookPath =
94
+ String(webhook?.path || "").trim() ||
95
+ (hookName ? `/hooks/${hookName}` : "/hooks/unknown");
96
+ const gatewayStatus =
97
+ request?.gatewayStatus == null ? "n/a" : String(request.gatewayStatus);
98
+ return [
99
+ "Investigate this failed webhook request and share findings before fixing anything.",
100
+ "Reply with your diagnosis first, including the likely root cause, any relevant risks, and what you would change if I approve a fix.",
101
+ "",
102
+ `Webhook: ${hookPath}`,
103
+ `Request ID: ${String(request?.id || "unknown")}`,
104
+ `Time: ${String(request?.createdAt || "unknown")}`,
105
+ `Method: ${String(request?.method || "unknown")}`,
106
+ `Source IP: ${String(request?.sourceIp || "unknown")}`,
107
+ `Gateway status: ${gatewayStatus}`,
108
+ `Transform path: ${String(webhook?.transformPath || "unknown")}`,
109
+ `Payload truncated: ${request?.payloadTruncated ? "yes" : "no"}`,
110
+ "",
111
+ "Headers:",
112
+ jsonPretty(request?.headers),
113
+ "",
114
+ "Payload:",
115
+ jsonPretty(request?.payload),
116
+ "",
117
+ "Gateway response:",
118
+ jsonPretty(request?.gatewayBody),
119
+ ].join("\n");
120
+ };
121
+
85
122
  const CreateWebhookModal = ({
86
123
  visible,
87
124
  name,
@@ -197,6 +234,8 @@ export const Webhooks = ({
197
234
  const [expandedRows, setExpandedRows] = useState(() => new Set());
198
235
  const [sendingTestWebhook, setSendingTestWebhook] = useState(false);
199
236
  const [replayingRequestId, setReplayingRequestId] = useState(null);
237
+ const [debugLoadingRequestId, setDebugLoadingRequestId] = useState(null);
238
+ const [debugRequest, setDebugRequest] = useState(null);
200
239
 
201
240
  const listPoll = usePolling(fetchWebhooks, 15000);
202
241
  const webhooks = listPoll.data?.webhooks || [];
@@ -496,6 +535,31 @@ export const Webhooks = ({
496
535
  }, []);
497
536
 
498
537
  const isListLoading = !listPoll.data && !listPoll.error;
538
+ const debugAgentMessage = useMemo(
539
+ () =>
540
+ buildWebhookDebugMessage({
541
+ hookName: selectedHookName,
542
+ webhook: selectedWebhook,
543
+ request: debugRequest,
544
+ }),
545
+ [debugRequest, selectedHookName, selectedWebhook],
546
+ );
547
+
548
+ const handleAskAgentToDebug = useCallback(
549
+ async (item) => {
550
+ if (!selectedHookName || !item?.id || debugLoadingRequestId === item.id) return;
551
+ try {
552
+ setDebugLoadingRequestId(item.id);
553
+ const data = await fetchWebhookRequest(selectedHookName, item.id);
554
+ setDebugRequest(data?.request || item);
555
+ } catch (err) {
556
+ showToast(err.message || "Could not load webhook request details", "error");
557
+ } finally {
558
+ setDebugLoadingRequestId(null);
559
+ }
560
+ },
561
+ [debugLoadingRequestId, selectedHookName],
562
+ );
499
563
 
500
564
  return html`
501
565
  <div class="space-y-4">
@@ -841,7 +905,7 @@ export const Webhooks = ({
841
905
  >
842
906
  ${jsonPretty(item.headers)}</pre
843
907
  >
844
- <div class="mt-2 flex justify-start">
908
+ <div class="mt-2 flex justify-start gap-2">
845
909
  <button
846
910
  class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
847
911
  onclick=${() =>
@@ -905,7 +969,7 @@ ${jsonPretty(item.payload)}</pre
905
969
  >
906
970
  ${jsonPretty(item.gatewayBody)}</pre
907
971
  >
908
- <div class="mt-2 flex justify-start">
972
+ <div class="mt-2 flex justify-start gap-2">
909
973
  <button
910
974
  class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
911
975
  onclick=${() =>
@@ -916,6 +980,19 @@ ${jsonPretty(item.gatewayBody)}</pre
916
980
  >
917
981
  Copy
918
982
  </button>
983
+ ${item.status === "error"
984
+ ? html`<${ActionButton}
985
+ onClick=${() =>
986
+ handleAskAgentToDebug(item)}
987
+ loading=${debugLoadingRequestId ===
988
+ item.id}
989
+ tone="primary"
990
+ size="sm"
991
+ idleLabel="Ask agent to debug"
992
+ loadingLabel="Loading..."
993
+ className="h-7 px-2.5"
994
+ />`
995
+ : null}
919
996
  </div>
920
997
  </div>
921
998
  </div>
@@ -1058,6 +1135,30 @@ ${jsonPretty(item.gatewayBody)}</pre
1058
1135
  }}
1059
1136
  onConfirm=${handleDeleteConfirmed}
1060
1137
  />
1138
+ <${AgentSendModal}
1139
+ visible=${!!debugRequest}
1140
+ title="Ask agent to debug"
1141
+ messageLabel="Debug request"
1142
+ messageRows=${12}
1143
+ initialMessage=${debugAgentMessage}
1144
+ resetKey=${String(debugRequest?.id || "")}
1145
+ submitLabel="Send debug request"
1146
+ loadingLabel="Sending..."
1147
+ onClose=${() => setDebugRequest(null)}
1148
+ onSubmit=${async ({ selectedSessionKey, message }) => {
1149
+ try {
1150
+ await sendAgentMessage({
1151
+ message,
1152
+ sessionKey: selectedSessionKey,
1153
+ });
1154
+ showToast("Debug request sent to agent", "success");
1155
+ return true;
1156
+ } catch (err) {
1157
+ showToast(err.message || "Could not send debug request", "error");
1158
+ return false;
1159
+ }
1160
+ }}
1161
+ />
1061
1162
  </div>
1062
1163
  `;
1063
1164
  };
@@ -101,7 +101,7 @@ export const Welcome = ({ onComplete }) => {
101
101
  : selectedProvider === "google"
102
102
  ? !!vals.GEMINI_API_KEY
103
103
  : selectedProvider === "openai-codex"
104
- ? !!(codexStatus.connected || vals.OPENAI_API_KEY)
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,
@@ -6,6 +6,7 @@ const { parsePositiveInt } = require("./utils/number");
6
6
  // Portable root directory: --root-dir flag sets ALPHACLAW_ROOT_DIR before require
7
7
  const kRootDir =
8
8
  process.env.ALPHACLAW_ROOT_DIR || path.join(os.homedir(), ".alphaclaw");
9
+ const ALPHACLAW_DIR = kRootDir;
9
10
  const kPackageRoot = path.resolve(__dirname, "..");
10
11
  const kNpmPackageRoot = path.resolve(kPackageRoot, "..");
11
12
  const kSetupDir = path.join(kPackageRoot, "setup");
@@ -18,6 +19,8 @@ const OPENCLAW_DIR = path.join(kRootDir, ".openclaw");
18
19
  const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || "";
19
20
  const ENV_FILE_PATH = path.join(kRootDir, ".env");
20
21
  const WORKSPACE_DIR = path.join(OPENCLAW_DIR, "workspace");
22
+ const kOnboardingMarkerPath = path.join(ALPHACLAW_DIR, "onboarded.json");
23
+ const kControlUiSkillPath = path.join(OPENCLAW_DIR, "skills", "control-ui", "SKILL.md");
21
24
  const AUTH_PROFILES_PATH = path.join(
22
25
  OPENCLAW_DIR,
23
26
  "agents",
@@ -153,26 +156,38 @@ const kKnownVars = [
153
156
  {
154
157
  key: "ANTHROPIC_API_KEY",
155
158
  label: "Anthropic API Key",
156
- group: "models",
159
+ group: "ai",
157
160
  hint: "From console.anthropic.com",
161
+ features: ["Models"],
158
162
  },
159
163
  {
160
164
  key: "ANTHROPIC_TOKEN",
161
165
  label: "Anthropic Setup Token",
162
- group: "models",
166
+ group: "ai",
163
167
  hint: "From claude setup-token",
168
+ features: ["Models"],
169
+ visibleInEnvars: false,
164
170
  },
165
171
  {
166
172
  key: "OPENAI_API_KEY",
167
173
  label: "OpenAI API Key",
168
- group: "models",
174
+ group: "ai",
169
175
  hint: "From platform.openai.com",
176
+ features: ["Models", "Embeddings", "TTS", "STT"],
170
177
  },
171
178
  {
172
179
  key: "GEMINI_API_KEY",
173
180
  label: "Gemini API Key",
174
- group: "models",
181
+ group: "ai",
175
182
  hint: "From aistudio.google.com",
183
+ features: ["Models", "Embeddings", "Image", "STT"],
184
+ },
185
+ {
186
+ key: "ELEVENLABS_API_KEY",
187
+ label: "ElevenLabs API Key",
188
+ group: "ai",
189
+ hint: "From elevenlabs.io (XI_API_KEY also works)",
190
+ features: ["TTS"],
176
191
  },
177
192
  {
178
193
  key: "GITHUB_TOKEN",
@@ -201,26 +216,30 @@ const kKnownVars = [
201
216
  {
202
217
  key: "MISTRAL_API_KEY",
203
218
  label: "Mistral API Key",
204
- group: "models",
219
+ group: "ai",
205
220
  hint: "From console.mistral.ai",
221
+ features: ["Models", "Embeddings", "STT"],
206
222
  },
207
223
  {
208
224
  key: "VOYAGE_API_KEY",
209
225
  label: "Voyage API Key",
210
- group: "models",
226
+ group: "ai",
211
227
  hint: "From dash.voyageai.com",
228
+ features: ["Embeddings"],
212
229
  },
213
230
  {
214
231
  key: "GROQ_API_KEY",
215
232
  label: "Groq API Key",
216
- group: "models",
233
+ group: "ai",
217
234
  hint: "From console.groq.com",
235
+ features: ["Models", "STT"],
218
236
  },
219
237
  {
220
238
  key: "DEEPGRAM_API_KEY",
221
239
  label: "Deepgram API Key",
222
- group: "models",
240
+ group: "ai",
223
241
  hint: "From console.deepgram.com",
242
+ features: ["STT"],
224
243
  },
225
244
  {
226
245
  key: "BRAVE_API_KEY",
@@ -330,6 +349,7 @@ const SETUP_API_PREFIXES = [
330
349
  ];
331
350
 
332
351
  module.exports = {
352
+ ALPHACLAW_DIR,
333
353
  kRootDir,
334
354
  kPackageRoot,
335
355
  kNpmPackageRoot,
@@ -342,6 +362,8 @@ module.exports = {
342
362
  GATEWAY_TOKEN,
343
363
  ENV_FILE_PATH,
344
364
  WORKSPACE_DIR,
365
+ kOnboardingMarkerPath,
366
+ kControlUiSkillPath,
345
367
  AUTH_PROFILES_PATH,
346
368
  CODEX_PROFILE_ID,
347
369
  CODEX_OAUTH_CLIENT_ID,
@@ -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);