@chrysb/alphaclaw 0.8.1-beta.7 → 0.8.1
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/css/shell.css +24 -0
- package/lib/public/js/app.js +1 -0
- package/lib/public/js/components/envars.js +7 -5
- package/lib/public/js/components/global-restart-banner.js +22 -9
- package/lib/public/js/components/nodes-tab/browser-attach/index.js +11 -21
- package/lib/public/js/components/nodes-tab/connected-nodes/use-connected-nodes-card.js +39 -3
- package/lib/public/js/hooks/use-app-shell-controller.js +35 -3
- package/lib/public/js/lib/api.js +7 -0
- package/lib/server/agents/agents.js +70 -28
- package/lib/server/onboarding/github.js +39 -0
- package/lib/server/routes/nodes.js +2 -2
- package/lib/server/routes/system.js +44 -4
- package/package.json +1 -1
package/lib/public/css/shell.css
CHANGED
|
@@ -44,6 +44,26 @@
|
|
|
44
44
|
flex-shrink: 0;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
.global-restart-banner__actions {
|
|
48
|
+
display: inline-flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 8px;
|
|
51
|
+
flex-shrink: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.global-restart-banner__dismiss {
|
|
55
|
+
display: inline-flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
padding: 2px;
|
|
59
|
+
color: #fde68a;
|
|
60
|
+
opacity: 0.85;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.global-restart-banner__dismiss:hover {
|
|
64
|
+
opacity: 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
.app-content {
|
|
48
68
|
grid-column: 3;
|
|
49
69
|
grid-row: 2;
|
|
@@ -392,6 +412,10 @@
|
|
|
392
412
|
position: static;
|
|
393
413
|
transform: none;
|
|
394
414
|
}
|
|
415
|
+
.global-restart-banner__actions {
|
|
416
|
+
width: 100%;
|
|
417
|
+
justify-content: flex-end;
|
|
418
|
+
}
|
|
395
419
|
.app-content {
|
|
396
420
|
grid-column: 1;
|
|
397
421
|
grid-row: 2;
|
package/lib/public/js/app.js
CHANGED
|
@@ -169,6 +169,7 @@ const App = () => {
|
|
|
169
169
|
visible=${controllerState.isAnyRestartRequired}
|
|
170
170
|
restarting=${controllerState.restartingGateway}
|
|
171
171
|
onRestart=${controllerActions.handleGatewayRestart}
|
|
172
|
+
onDismiss=${controllerActions.dismissRestartBanner}
|
|
172
173
|
/>
|
|
173
174
|
<${AppSidebar}
|
|
174
175
|
mobileSidebarOpen=${shellState.mobileSidebarOpen}
|
|
@@ -333,7 +333,9 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
333
333
|
setVars(nextVars);
|
|
334
334
|
setPendingCustomKeys([]);
|
|
335
335
|
setReservedKeys(new Set(data.reservedKeys || []));
|
|
336
|
-
|
|
336
|
+
if (data.restartRequired) {
|
|
337
|
+
onRestartRequired(true);
|
|
338
|
+
}
|
|
337
339
|
},
|
|
338
340
|
[onRestartRequired],
|
|
339
341
|
);
|
|
@@ -389,11 +391,11 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
389
391
|
: "Environment variables saved",
|
|
390
392
|
"success",
|
|
391
393
|
);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
394
|
+
// Force-refresh /api/env so stale cached payload cannot overwrite newly
|
|
395
|
+
// saved values with older state right after save.
|
|
396
|
+
const latestPayload = await refreshEnvPayload({ force: true });
|
|
397
|
+
applyEnvPayload(latestPayload);
|
|
395
398
|
setSecretMaskEpoch((prev) => prev + 1);
|
|
396
|
-
baselineSignatureRef.current = getVarsSignature(sortedVars);
|
|
397
399
|
setDirty(false);
|
|
398
400
|
} catch (err) {
|
|
399
401
|
showToast("Failed to save: " + err.message, "error");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import htm from "https://esm.sh/htm";
|
|
3
3
|
import { UpdateActionButton } from "./update-action-button.js";
|
|
4
|
+
import { CloseIcon } from "./icons.js";
|
|
4
5
|
|
|
5
6
|
const html = htm.bind(h);
|
|
6
7
|
|
|
@@ -8,6 +9,7 @@ export const GlobalRestartBanner = ({
|
|
|
8
9
|
visible = false,
|
|
9
10
|
restarting = false,
|
|
10
11
|
onRestart,
|
|
12
|
+
onDismiss = () => {},
|
|
11
13
|
}) => {
|
|
12
14
|
if (!visible) return null;
|
|
13
15
|
return html`
|
|
@@ -16,15 +18,26 @@ export const GlobalRestartBanner = ({
|
|
|
16
18
|
<p class="global-restart-banner__text">
|
|
17
19
|
Gateway restart required to apply pending configuration changes.
|
|
18
20
|
</p>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
<div class="global-restart-banner__actions">
|
|
22
|
+
<${UpdateActionButton}
|
|
23
|
+
onClick=${onRestart}
|
|
24
|
+
disabled=${restarting}
|
|
25
|
+
loading=${restarting}
|
|
26
|
+
warning=${true}
|
|
27
|
+
idleLabel="Restart Gateway"
|
|
28
|
+
loadingLabel="Restarting..."
|
|
29
|
+
className="global-restart-banner__button"
|
|
30
|
+
/>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
onclick=${onDismiss}
|
|
34
|
+
class="global-restart-banner__dismiss ac-btn-ghost"
|
|
35
|
+
aria-label="Dismiss restart banner"
|
|
36
|
+
title="Dismiss"
|
|
37
|
+
>
|
|
38
|
+
<${CloseIcon} className="h-3.5 w-3.5" />
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
28
41
|
</div>
|
|
29
42
|
</div>
|
|
30
43
|
`;
|
|
@@ -60,26 +60,16 @@ export const BrowserAttachCard = () => {
|
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
return html`
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class="cursor-pointer text-xs text-gray-300 hover:text-gray-200"
|
|
75
|
-
>
|
|
76
|
-
Setup instructions
|
|
77
|
-
</summary>
|
|
78
|
-
<div
|
|
79
|
-
class="pt-3 file-viewer-preview release-notes-preview text-xs leading-5"
|
|
80
|
-
dangerouslySetInnerHTML=${{ __html: setupInstructionsHtml }}
|
|
81
|
-
></div>
|
|
82
|
-
</details>
|
|
83
|
-
</div>
|
|
63
|
+
<details
|
|
64
|
+
class="ac-surface-inset rounded-lg border border-border px-3 py-2.5"
|
|
65
|
+
>
|
|
66
|
+
<summary class="cursor-pointer text-xs text-gray-300 hover:text-gray-200">
|
|
67
|
+
Chrome debugging setup / troubleshooting
|
|
68
|
+
</summary>
|
|
69
|
+
<div
|
|
70
|
+
class="pt-3 px-2 file-viewer-preview release-notes-preview text-xs leading-5"
|
|
71
|
+
dangerouslySetInnerHTML=${{ __html: setupInstructionsHtml }}
|
|
72
|
+
></div>
|
|
73
|
+
</details>
|
|
84
74
|
`;
|
|
85
75
|
};
|
|
@@ -9,7 +9,7 @@ import { fetchNodeBrowserStatusForNode, removeNode } from "../../../lib/api.js";
|
|
|
9
9
|
import { readUiSettings, updateUiSettings } from "../../../lib/ui-settings.js";
|
|
10
10
|
import { showToast } from "../../toast.js";
|
|
11
11
|
|
|
12
|
-
const kBrowserCheckTimeoutMs =
|
|
12
|
+
const kBrowserCheckTimeoutMs = 35000;
|
|
13
13
|
const kBrowserPollIntervalMs = 10000;
|
|
14
14
|
const kBrowserAttachStateByNodeKey = "nodesBrowserAttachStateByNode";
|
|
15
15
|
|
|
@@ -212,6 +212,32 @@ export const useConnectedNodesCard = ({
|
|
|
212
212
|
removingNodeId,
|
|
213
213
|
]);
|
|
214
214
|
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (checkingBrowserNodeId) return;
|
|
217
|
+
const pendingInitialNodeId = nodes
|
|
218
|
+
.map((node) => ({
|
|
219
|
+
nodeId: String(node?.nodeId || "").trim(),
|
|
220
|
+
connected: node?.connected === true,
|
|
221
|
+
browserCapable: isBrowserCapableNode(node),
|
|
222
|
+
}))
|
|
223
|
+
.find((entry) => {
|
|
224
|
+
if (!entry.nodeId || !entry.connected || !entry.browserCapable) return false;
|
|
225
|
+
if (browserAttachStateByNodeId?.[entry.nodeId] !== true) return false;
|
|
226
|
+
if (browserStatusByNodeId?.[entry.nodeId]) return false;
|
|
227
|
+
if (browserErrorByNodeId?.[entry.nodeId]) return false;
|
|
228
|
+
return true;
|
|
229
|
+
})?.nodeId;
|
|
230
|
+
if (!pendingInitialNodeId) return;
|
|
231
|
+
handleCheckNodeBrowser(pendingInitialNodeId, { silent: true });
|
|
232
|
+
}, [
|
|
233
|
+
browserAttachStateByNodeId,
|
|
234
|
+
browserErrorByNodeId,
|
|
235
|
+
browserStatusByNodeId,
|
|
236
|
+
checkingBrowserNodeId,
|
|
237
|
+
handleCheckNodeBrowser,
|
|
238
|
+
nodes,
|
|
239
|
+
]);
|
|
240
|
+
|
|
215
241
|
useEffect(() => {
|
|
216
242
|
if (checkingBrowserNodeId) return;
|
|
217
243
|
const pollableNodeIds = nodes
|
|
@@ -219,13 +245,17 @@ export const useConnectedNodesCard = ({
|
|
|
219
245
|
nodeId: String(node?.nodeId || "").trim(),
|
|
220
246
|
connected: node?.connected === true,
|
|
221
247
|
browserCapable: isBrowserCapableNode(node),
|
|
248
|
+
browserRunning:
|
|
249
|
+
browserStatusByNodeId?.[String(node?.nodeId || "").trim()]?.running ===
|
|
250
|
+
true,
|
|
222
251
|
}))
|
|
223
252
|
.filter(
|
|
224
253
|
(entry) =>
|
|
225
254
|
entry.nodeId &&
|
|
226
255
|
entry.connected &&
|
|
227
256
|
entry.browserCapable &&
|
|
228
|
-
browserAttachStateByNodeId?.[entry.nodeId] === true
|
|
257
|
+
browserAttachStateByNodeId?.[entry.nodeId] === true &&
|
|
258
|
+
entry.browserRunning,
|
|
229
259
|
)
|
|
230
260
|
.map((entry) => entry.nodeId);
|
|
231
261
|
if (!pollableNodeIds.length) return;
|
|
@@ -244,7 +274,13 @@ export const useConnectedNodesCard = ({
|
|
|
244
274
|
active = false;
|
|
245
275
|
clearInterval(timer);
|
|
246
276
|
};
|
|
247
|
-
}, [
|
|
277
|
+
}, [
|
|
278
|
+
browserAttachStateByNodeId,
|
|
279
|
+
browserStatusByNodeId,
|
|
280
|
+
checkingBrowserNodeId,
|
|
281
|
+
handleCheckNodeBrowser,
|
|
282
|
+
nodes,
|
|
283
|
+
]);
|
|
248
284
|
|
|
249
285
|
return {
|
|
250
286
|
browserStatusByNodeId,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
fetchAlphaclawVersion,
|
|
7
7
|
updateAlphaclaw,
|
|
8
8
|
fetchRestartStatus,
|
|
9
|
+
dismissRestartStatus,
|
|
9
10
|
restartGateway,
|
|
10
11
|
fetchWatchdogStatus,
|
|
11
12
|
fetchDoctorStatus,
|
|
@@ -17,6 +18,7 @@ import { usePolling } from "./usePolling.js";
|
|
|
17
18
|
import { showToast } from "../components/toast.js";
|
|
18
19
|
|
|
19
20
|
export const useAppShellController = ({ location = "" } = {}) => {
|
|
21
|
+
const kInitialStatusPollDelayMs = 5000;
|
|
20
22
|
const [onboarded, setOnboarded] = useState(null);
|
|
21
23
|
const [authEnabled, setAuthEnabled] = useState(false);
|
|
22
24
|
const [acVersion, setAcVersion] = useState(null);
|
|
@@ -28,6 +30,7 @@ export const useAppShellController = ({ location = "" } = {}) => {
|
|
|
28
30
|
const [restartingGateway, setRestartingGateway] = useState(false);
|
|
29
31
|
const [gatewayRestartSignal, setGatewayRestartSignal] = useState(0);
|
|
30
32
|
const [statusPollCadenceMs, setStatusPollCadenceMs] = useState(15000);
|
|
33
|
+
const [statusPollingGraceElapsed, setStatusPollingGraceElapsed] = useState(false);
|
|
31
34
|
const [openclawUpdateInProgress, setOpenclawUpdateInProgress] = useState(false);
|
|
32
35
|
const [statusStreamConnected, setStatusStreamConnected] = useState(false);
|
|
33
36
|
const [statusStreamStatus, setStatusStreamStatus] = useState(null);
|
|
@@ -35,15 +38,18 @@ export const useAppShellController = ({ location = "" } = {}) => {
|
|
|
35
38
|
const [statusStreamDoctor, setStatusStreamDoctor] = useState(null);
|
|
36
39
|
|
|
37
40
|
const sharedStatusPoll = usePolling(fetchStatus, statusPollCadenceMs, {
|
|
38
|
-
enabled:
|
|
41
|
+
enabled:
|
|
42
|
+
onboarded === true && !statusStreamConnected && statusPollingGraceElapsed,
|
|
39
43
|
cacheKey: "/api/status",
|
|
40
44
|
});
|
|
41
45
|
const sharedWatchdogPoll = usePolling(fetchWatchdogStatus, statusPollCadenceMs, {
|
|
42
|
-
enabled:
|
|
46
|
+
enabled:
|
|
47
|
+
onboarded === true && !statusStreamConnected && statusPollingGraceElapsed,
|
|
43
48
|
cacheKey: "/api/watchdog/status",
|
|
44
49
|
});
|
|
45
50
|
const sharedDoctorPoll = usePolling(fetchDoctorStatus, statusPollCadenceMs, {
|
|
46
|
-
enabled:
|
|
51
|
+
enabled:
|
|
52
|
+
onboarded === true && !statusStreamConnected && statusPollingGraceElapsed,
|
|
47
53
|
cacheKey: "/api/doctor/status",
|
|
48
54
|
});
|
|
49
55
|
const sharedStatus = statusStreamStatus || sharedStatusPoll.data || null;
|
|
@@ -68,6 +74,19 @@ export const useAppShellController = ({ location = "" } = {}) => {
|
|
|
68
74
|
.catch(() => {});
|
|
69
75
|
}, []);
|
|
70
76
|
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (onboarded !== true) {
|
|
79
|
+
setStatusPollingGraceElapsed(false);
|
|
80
|
+
return () => {};
|
|
81
|
+
}
|
|
82
|
+
const timerId = setTimeout(() => {
|
|
83
|
+
setStatusPollingGraceElapsed(true);
|
|
84
|
+
}, kInitialStatusPollDelayMs);
|
|
85
|
+
return () => {
|
|
86
|
+
clearTimeout(timerId);
|
|
87
|
+
};
|
|
88
|
+
}, [onboarded]);
|
|
89
|
+
|
|
71
90
|
useEffect(() => {
|
|
72
91
|
if (onboarded !== true) return;
|
|
73
92
|
let disposed = false;
|
|
@@ -261,6 +280,18 @@ export const useAppShellController = ({ location = "" } = {}) => {
|
|
|
261
280
|
}
|
|
262
281
|
}, [acUpdating]);
|
|
263
282
|
|
|
283
|
+
const dismissRestartBanner = useCallback(async () => {
|
|
284
|
+
setRestartRequired(false);
|
|
285
|
+
setBrowseRestartRequired(false);
|
|
286
|
+
try {
|
|
287
|
+
await dismissRestartStatus();
|
|
288
|
+
await refreshRestartStatus();
|
|
289
|
+
} catch (err) {
|
|
290
|
+
showToast(err.message || "Could not dismiss restart banner", "error");
|
|
291
|
+
await refreshRestartStatus();
|
|
292
|
+
}
|
|
293
|
+
}, [refreshRestartStatus]);
|
|
294
|
+
|
|
264
295
|
return {
|
|
265
296
|
state: {
|
|
266
297
|
acHasUpdate,
|
|
@@ -284,6 +315,7 @@ export const useAppShellController = ({ location = "" } = {}) => {
|
|
|
284
315
|
handleOpenclawUpdate,
|
|
285
316
|
handleOpenclawVersionActionComplete,
|
|
286
317
|
refreshSharedStatuses,
|
|
318
|
+
dismissRestartBanner,
|
|
287
319
|
setRestartRequired,
|
|
288
320
|
},
|
|
289
321
|
};
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -346,6 +346,13 @@ export async function fetchRestartStatus() {
|
|
|
346
346
|
return parseJsonOrThrow(res, "Could not load restart status");
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
export async function dismissRestartStatus() {
|
|
350
|
+
const res = await authFetch("/api/restart-status/dismiss", {
|
|
351
|
+
method: "POST",
|
|
352
|
+
});
|
|
353
|
+
return parseJsonOrThrow(res, "Could not dismiss restart status");
|
|
354
|
+
}
|
|
355
|
+
|
|
349
356
|
export async function fetchWatchdogStatus() {
|
|
350
357
|
const res = await authFetch("/api/watchdog/status");
|
|
351
358
|
return parseJsonOrThrow(res, "Could not load watchdog status");
|
|
@@ -14,18 +14,40 @@ const {
|
|
|
14
14
|
ensureAgentScaffold,
|
|
15
15
|
} = require("./shared");
|
|
16
16
|
|
|
17
|
+
const toTitleWords = (value = "") =>
|
|
18
|
+
String(value || "")
|
|
19
|
+
.trim()
|
|
20
|
+
.split(/[-_\s]+/)
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
23
|
+
.join(" ");
|
|
24
|
+
|
|
25
|
+
const getFallbackAgentName = (agentId = "") => {
|
|
26
|
+
const normalizedAgentId = String(agentId || "").trim();
|
|
27
|
+
if (!normalizedAgentId) return "Agent";
|
|
28
|
+
const title = toTitleWords(normalizedAgentId) || normalizedAgentId;
|
|
29
|
+
return `${title} Agent`;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const getAgentDisplayName = (agent = {}) =>
|
|
33
|
+
String(agent?.identity?.name || "").trim() ||
|
|
34
|
+
String(agent?.name || "").trim() ||
|
|
35
|
+
getFallbackAgentName(agent?.id || "");
|
|
36
|
+
|
|
37
|
+
const toReadableAgent = (agent = {}) => ({
|
|
38
|
+
...agent,
|
|
39
|
+
id: String(agent.id || "").trim(),
|
|
40
|
+
name: getAgentDisplayName(agent),
|
|
41
|
+
default: !!agent.default,
|
|
42
|
+
});
|
|
43
|
+
|
|
17
44
|
const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
|
|
18
45
|
const listAgents = () => {
|
|
19
46
|
const cfg = withNormalizedAgentsConfig({
|
|
20
47
|
OPENCLAW_DIR,
|
|
21
48
|
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
22
49
|
});
|
|
23
|
-
return (cfg.agents?.list || []).map((entry) => (
|
|
24
|
-
...entry,
|
|
25
|
-
id: String(entry.id || "").trim(),
|
|
26
|
-
name: String(entry.name || "").trim() || String(entry.id || "").trim(),
|
|
27
|
-
default: !!entry.default,
|
|
28
|
-
}));
|
|
50
|
+
return (cfg.agents?.list || []).map((entry) => toReadableAgent(entry));
|
|
29
51
|
};
|
|
30
52
|
|
|
31
53
|
const getAgent = (agentId) => {
|
|
@@ -84,20 +106,29 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
|
|
|
84
106
|
OPENCLAW_DIR,
|
|
85
107
|
agentId,
|
|
86
108
|
});
|
|
109
|
+
const requestedIdentity =
|
|
110
|
+
input.identity && typeof input.identity === "object"
|
|
111
|
+
? { ...input.identity }
|
|
112
|
+
: {};
|
|
113
|
+
const requestedName = String(input.name || "").trim();
|
|
114
|
+
const identityName =
|
|
115
|
+
requestedName ||
|
|
116
|
+
String(requestedIdentity.name || "").trim() ||
|
|
117
|
+
getFallbackAgentName(agentId);
|
|
87
118
|
const nextAgent = {
|
|
88
119
|
id: agentId,
|
|
89
|
-
name: String(input.name || "").trim() || agentId,
|
|
90
120
|
default: false,
|
|
91
121
|
workspace: scaffoldWorkspacePath,
|
|
92
122
|
agentDir: agentDirPath,
|
|
123
|
+
identity: {
|
|
124
|
+
...requestedIdentity,
|
|
125
|
+
name: identityName,
|
|
126
|
+
},
|
|
93
127
|
...(input.model ? { model: input.model } : {}),
|
|
94
|
-
...(input.identity && typeof input.identity === "object"
|
|
95
|
-
? { identity: { ...input.identity } }
|
|
96
|
-
: {}),
|
|
97
128
|
};
|
|
98
129
|
cfg.agents.list = [...cfg.agents.list, nextAgent];
|
|
99
130
|
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
100
|
-
return nextAgent;
|
|
131
|
+
return toReadableAgent(nextAgent);
|
|
101
132
|
};
|
|
102
133
|
|
|
103
134
|
const updateAgent = (agentId, patch = {}) => {
|
|
@@ -109,20 +140,29 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
|
|
|
109
140
|
const index = cfg.agents.list.findIndex((entry) => entry.id === normalized);
|
|
110
141
|
if (index < 0) throw new Error(`Agent "${normalized}" not found`);
|
|
111
142
|
const current = cfg.agents.list[index];
|
|
112
|
-
const next = {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
identity
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
const next = { ...current };
|
|
144
|
+
const identityPatched =
|
|
145
|
+
patch.identity !== undefined || patch.name !== undefined;
|
|
146
|
+
if (identityPatched) {
|
|
147
|
+
const baseIdentity =
|
|
148
|
+
patch.identity !== undefined
|
|
149
|
+
? patch.identity && typeof patch.identity === "object"
|
|
150
|
+
? { ...patch.identity }
|
|
151
|
+
: {}
|
|
152
|
+
: current.identity && typeof current.identity === "object"
|
|
153
|
+
? { ...current.identity }
|
|
154
|
+
: {};
|
|
155
|
+
const requestedName =
|
|
156
|
+
patch.name !== undefined
|
|
157
|
+
? String(patch.name || "").trim()
|
|
158
|
+
: String(baseIdentity.name || "").trim();
|
|
159
|
+
const fallbackLegacyName = String(current.name || "").trim();
|
|
160
|
+
baseIdentity.name =
|
|
161
|
+
requestedName || fallbackLegacyName || getFallbackAgentName(normalized);
|
|
162
|
+
next.identity = baseIdentity;
|
|
163
|
+
// Only remove legacy top-level name once identity.name is persisted.
|
|
164
|
+
delete next.name;
|
|
165
|
+
}
|
|
126
166
|
if (patch.model !== undefined) {
|
|
127
167
|
if (patch.model === null) {
|
|
128
168
|
delete next.model;
|
|
@@ -134,7 +174,10 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
|
|
|
134
174
|
if (patch.tools && typeof patch.tools === "object") {
|
|
135
175
|
const toolsCfg = {};
|
|
136
176
|
if (patch.tools.profile) toolsCfg.profile = String(patch.tools.profile);
|
|
137
|
-
if (
|
|
177
|
+
if (
|
|
178
|
+
Array.isArray(patch.tools.alsoAllow) &&
|
|
179
|
+
patch.tools.alsoAllow.length
|
|
180
|
+
) {
|
|
138
181
|
toolsCfg.alsoAllow = patch.tools.alsoAllow.map(String);
|
|
139
182
|
}
|
|
140
183
|
if (Array.isArray(patch.tools.deny) && patch.tools.deny.length) {
|
|
@@ -145,10 +188,9 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
|
|
|
145
188
|
delete next.tools;
|
|
146
189
|
}
|
|
147
190
|
}
|
|
148
|
-
if (!String(next.name || "").trim()) next.name = normalized;
|
|
149
191
|
cfg.agents.list[index] = next;
|
|
150
192
|
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
151
|
-
return next;
|
|
193
|
+
return toReadableAgent(next);
|
|
152
194
|
};
|
|
153
195
|
|
|
154
196
|
const setDefaultAgent = (agentId) => {
|
|
@@ -31,6 +31,38 @@ const parseGithubErrorMessage = async (response) => {
|
|
|
31
31
|
return response.statusText || `HTTP ${response.status}`;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// Files GitHub may auto-create when initializing a repo — a repo containing
|
|
35
|
+
// only these is treated as empty for onboarding purposes.
|
|
36
|
+
const kBoilerplateNames = new Set([
|
|
37
|
+
"readme",
|
|
38
|
+
"readme.md",
|
|
39
|
+
"readme.txt",
|
|
40
|
+
"readme.rst",
|
|
41
|
+
"license",
|
|
42
|
+
"license.md",
|
|
43
|
+
"license.txt",
|
|
44
|
+
".gitignore",
|
|
45
|
+
".gitattributes",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const repoContainsOnlyBoilerplate = async (repoUrl, ghHeaders) => {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(
|
|
51
|
+
`https://api.github.com/repos/${repoUrl}/contents/`,
|
|
52
|
+
{ headers: ghHeaders },
|
|
53
|
+
);
|
|
54
|
+
if (!res.ok) return false;
|
|
55
|
+
const entries = await res.json();
|
|
56
|
+
if (!Array.isArray(entries)) return false;
|
|
57
|
+
if (entries.length === 0) return true;
|
|
58
|
+
return entries.every(
|
|
59
|
+
(e) => e.type === "file" && kBoilerplateNames.has(e.name.toLowerCase()),
|
|
60
|
+
);
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
34
66
|
const isClassicPat = (token) => String(token || "").startsWith("ghp_");
|
|
35
67
|
const isFineGrainedPat = (token) =>
|
|
36
68
|
String(token || "").startsWith("github_pat_");
|
|
@@ -98,6 +130,13 @@ const verifyGithubRepoForOnboarding = async ({
|
|
|
98
130
|
return { ok: true, repoExists: true, repoIsEmpty: true };
|
|
99
131
|
}
|
|
100
132
|
if (commitsRes.ok) {
|
|
133
|
+
const onlyBoilerplate = await repoContainsOnlyBoilerplate(
|
|
134
|
+
repoUrl,
|
|
135
|
+
ghHeaders,
|
|
136
|
+
);
|
|
137
|
+
if (onlyBoilerplate) {
|
|
138
|
+
return { ok: true, repoExists: true, repoIsEmpty: true };
|
|
139
|
+
}
|
|
101
140
|
if (isExisting) {
|
|
102
141
|
return { ok: true, repoExists: true, repoIsEmpty: false };
|
|
103
142
|
}
|
|
@@ -7,8 +7,8 @@ const kAllowedExecHosts = new Set(["gateway", "node"]);
|
|
|
7
7
|
const kAllowedExecSecurity = new Set(["deny", "allowlist", "full"]);
|
|
8
8
|
const kAllowedExecAsk = new Set(["off", "on-miss", "always"]);
|
|
9
9
|
const kSafeNodeIdPattern = /^[\w\-:.]+$/;
|
|
10
|
-
const kNodeBrowserInvokeTimeoutMs =
|
|
11
|
-
const kNodeBrowserCliTimeoutMs =
|
|
10
|
+
const kNodeBrowserInvokeTimeoutMs = 30000;
|
|
11
|
+
const kNodeBrowserCliTimeoutMs = 35000;
|
|
12
12
|
const kNodeRouteCliTimeoutMs = 12000;
|
|
13
13
|
|
|
14
14
|
const quoteCliArg = (value) => quoteShellArg(value, { strategy: "single" });
|
|
@@ -151,16 +151,40 @@ const registerSystemRoutes = ({
|
|
|
151
151
|
}
|
|
152
152
|
return "Telegram";
|
|
153
153
|
};
|
|
154
|
-
const
|
|
154
|
+
const getDefaultAgentLabel = (config = {}) => {
|
|
155
|
+
return "Main Agent";
|
|
156
|
+
};
|
|
157
|
+
const getFallbackAgentLabel = (agentId = "") => {
|
|
158
|
+
const normalizedAgentId = String(agentId || "").trim();
|
|
159
|
+
if (!normalizedAgentId) return "Agent";
|
|
160
|
+
const titledAgentId = toTitleWords(normalizedAgentId) || normalizedAgentId;
|
|
161
|
+
return `${titledAgentId} Agent`;
|
|
162
|
+
};
|
|
163
|
+
const getConfiguredAgentLabel = (config = {}, agentId = "") => {
|
|
164
|
+
const normalizedAgentId = String(agentId || "").trim();
|
|
165
|
+
if (!normalizedAgentId) return "Agent";
|
|
166
|
+
const configuredAgents = Array.isArray(config?.agents?.list)
|
|
167
|
+
? config.agents.list
|
|
168
|
+
: [];
|
|
169
|
+
const configuredAgent = configuredAgents.find(
|
|
170
|
+
(entry) => String(entry?.id || "").trim() === normalizedAgentId,
|
|
171
|
+
);
|
|
172
|
+
const configuredName =
|
|
173
|
+
String(configuredAgent?.name || "").trim() ||
|
|
174
|
+
String(configuredAgent?.identity?.name || "").trim();
|
|
175
|
+
if (configuredName) return configuredName;
|
|
176
|
+
if (normalizedAgentId === "main") return getDefaultAgentLabel(config);
|
|
177
|
+
return getFallbackAgentLabel(normalizedAgentId);
|
|
178
|
+
};
|
|
179
|
+
const getAgentLabelFromSessionKey = (key = "", config = {}) => {
|
|
155
180
|
const match = String(key || "").match(/^agent:([^:]+):/);
|
|
156
181
|
const agentId = String(match?.[1] || "").trim();
|
|
157
182
|
if (!agentId) return "Agent";
|
|
158
|
-
|
|
159
|
-
return toTitleWords(agentId);
|
|
183
|
+
return getConfiguredAgentLabel(config, agentId);
|
|
160
184
|
};
|
|
161
185
|
const buildSessionLabel = (sessionRow = {}, config = {}) => {
|
|
162
186
|
const key = String(sessionRow?.key || "");
|
|
163
|
-
const agentLabel = getAgentLabelFromSessionKey(key);
|
|
187
|
+
const agentLabel = getAgentLabelFromSessionKey(key, config);
|
|
164
188
|
const agentKeyMatch = key.match(/^agent:([^:]+):/);
|
|
165
189
|
const agentId = String(agentKeyMatch?.[1] || "").trim();
|
|
166
190
|
const telegramChannelName = resolveTelegramChannelNameForAgent({
|
|
@@ -749,6 +773,22 @@ const registerSystemRoutes = ({
|
|
|
749
773
|
}
|
|
750
774
|
});
|
|
751
775
|
|
|
776
|
+
app.post("/api/restart-status/dismiss", async (req, res) => {
|
|
777
|
+
try {
|
|
778
|
+
envRestartPending = false;
|
|
779
|
+
restartRequiredState.clearRequired();
|
|
780
|
+
const snapshot = await restartRequiredState.getSnapshot();
|
|
781
|
+
res.json({
|
|
782
|
+
ok: true,
|
|
783
|
+
restartRequired: snapshot.restartRequired || envRestartPending,
|
|
784
|
+
restartInProgress: snapshot.restartInProgress,
|
|
785
|
+
gatewayRunning: snapshot.gatewayRunning,
|
|
786
|
+
});
|
|
787
|
+
} catch (err) {
|
|
788
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
752
792
|
app.post("/api/gateway/restart", async (req, res) => {
|
|
753
793
|
if (!isOnboarded()) {
|
|
754
794
|
return res.status(400).json({ ok: false, error: "Not onboarded" });
|