@chrysb/alphaclaw 0.8.4 → 0.8.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/css/explorer.css +48 -0
- package/lib/public/css/shell.css +149 -0
- package/lib/public/css/tailwind.generated.css +1 -1
- package/lib/public/css/theme.css +265 -0
- package/lib/public/dist/app.bundle.js +2269 -2200
- package/lib/public/js/app.js +4 -0
- package/lib/public/js/components/icons.js +38 -0
- package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
- package/lib/public/js/components/models-tab/use-models.js +74 -9
- package/lib/public/js/components/models.js +52 -37
- package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
- package/lib/public/js/components/onboarding/welcome-config.js +76 -10
- package/lib/public/js/components/onboarding/welcome-form-step.js +2 -7
- package/lib/public/js/components/onboarding/welcome-header.js +12 -14
- package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
- package/lib/public/js/components/providers.js +53 -42
- package/lib/public/js/components/sidebar.js +9 -1
- package/lib/public/js/components/theme-toggle.js +113 -0
- package/lib/public/js/components/welcome/index.js +0 -2
- package/lib/public/js/components/welcome/use-welcome.js +101 -36
- package/lib/public/js/lib/codex-oauth-window.js +22 -0
- package/lib/public/js/lib/model-catalog.js +20 -0
- package/lib/public/js/lib/storage-keys.js +1 -1
- package/lib/public/login.html +8 -4
- package/lib/public/setup.html +9 -0
- package/lib/server/alphaclaw-version.js +60 -13
- package/lib/server/db/webhooks/index.js +48 -8
- package/lib/server/model-catalog-cache.js +251 -0
- package/lib/server/routes/models.js +14 -23
- package/lib/server/routes/webhooks.js +12 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
applyImport,
|
|
7
7
|
fetchModels,
|
|
8
8
|
} from "../../lib/api.js";
|
|
9
|
+
import { useCachedFetch } from "../../hooks/use-cached-fetch.js";
|
|
9
10
|
import {
|
|
10
11
|
getModelProvider,
|
|
11
12
|
getAuthProviderFromModelProvider,
|
|
@@ -13,8 +14,15 @@ import {
|
|
|
13
14
|
getVisibleAiFieldKeys,
|
|
14
15
|
kProviderAuthFields,
|
|
15
16
|
} from "../../lib/model-config.js";
|
|
17
|
+
import {
|
|
18
|
+
getInitialOnboardingModelKey,
|
|
19
|
+
getModelCatalogModels,
|
|
20
|
+
kModelCatalogCacheKey,
|
|
21
|
+
} from "../../lib/model-catalog.js";
|
|
16
22
|
import {
|
|
17
23
|
kWelcomeGroups,
|
|
24
|
+
getWelcomeGroupError,
|
|
25
|
+
findFirstInvalidWelcomeGroup,
|
|
18
26
|
isValidGithubRepoInput,
|
|
19
27
|
kGithubFlowFresh,
|
|
20
28
|
kGithubFlowImport,
|
|
@@ -76,11 +84,18 @@ const normalizePlaceholderReview = (review) => {
|
|
|
76
84
|
export const useWelcome = ({ onComplete }) => {
|
|
77
85
|
const kSetupStepIndex = kWelcomeGroups.length;
|
|
78
86
|
const kPairingStepIndex = kSetupStepIndex + 1;
|
|
79
|
-
const {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const {
|
|
88
|
+
vals,
|
|
89
|
+
setVals,
|
|
90
|
+
setValue: setStoredValue,
|
|
91
|
+
step,
|
|
92
|
+
setStep,
|
|
93
|
+
setupError,
|
|
94
|
+
setSetupError,
|
|
95
|
+
} = useWelcomeStorage({
|
|
96
|
+
kSetupStepIndex,
|
|
97
|
+
kPairingStepIndex,
|
|
98
|
+
});
|
|
84
99
|
const [models, setModels] = useState([]);
|
|
85
100
|
const [modelsLoading, setModelsLoading] = useState(true);
|
|
86
101
|
const [modelsError, setModelsError] = useState(null);
|
|
@@ -110,6 +125,14 @@ export const useWelcome = ({ onComplete }) => {
|
|
|
110
125
|
const [importScanResult, setImportScanResult] = useState(null);
|
|
111
126
|
const [importScanning, setImportScanning] = useState(false);
|
|
112
127
|
const [importError, setImportError] = useState(null);
|
|
128
|
+
const modelsFetchState = useCachedFetch(kModelCatalogCacheKey, fetchModels, {
|
|
129
|
+
maxAgeMs: 30000,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const setValue = (key, value) => {
|
|
133
|
+
if (formError) setFormError(null);
|
|
134
|
+
setStoredValue(key, value);
|
|
135
|
+
};
|
|
113
136
|
|
|
114
137
|
const setImportStep = (nextStep) => {
|
|
115
138
|
setImportStepState(nextStep);
|
|
@@ -129,21 +152,54 @@ export const useWelcome = ({ onComplete }) => {
|
|
|
129
152
|
};
|
|
130
153
|
|
|
131
154
|
useEffect(() => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
const list = getModelCatalogModels(modelsFetchState.data);
|
|
156
|
+
if (!modelsFetchState.data) return;
|
|
157
|
+
setModels(list);
|
|
158
|
+
setModelsError(list.length > 0 ? null : "No models found");
|
|
159
|
+
const defaultModelKey = getInitialOnboardingModelKey({
|
|
160
|
+
catalog: list,
|
|
161
|
+
currentModelKey: vals.MODEL_KEY,
|
|
162
|
+
});
|
|
163
|
+
if (!vals.MODEL_KEY && defaultModelKey) {
|
|
164
|
+
setVals((prev) => ({ ...prev, MODEL_KEY: defaultModelKey }));
|
|
165
|
+
}
|
|
166
|
+
}, [modelsFetchState.data, setVals, vals.MODEL_KEY]);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const hasModels = getModelCatalogModels(modelsFetchState.data).length > 0;
|
|
170
|
+
setModelsLoading(modelsFetchState.loading && !hasModels);
|
|
171
|
+
}, [modelsFetchState.data, modelsFetchState.loading]);
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (!modelsFetchState.error) return;
|
|
175
|
+
setModelsError("Failed to load models");
|
|
176
|
+
setModelsLoading(false);
|
|
177
|
+
}, [modelsFetchState.error]);
|
|
178
|
+
|
|
179
|
+
const getValidationContext = (currentVals = {}) => {
|
|
180
|
+
const currentSelectedProvider = getModelProvider(
|
|
181
|
+
String(currentVals.MODEL_KEY || "").trim(),
|
|
182
|
+
);
|
|
183
|
+
const currentSelectedAuthProvider =
|
|
184
|
+
getAuthProviderFromModelProvider(currentSelectedProvider);
|
|
185
|
+
const currentProviderAuthFields =
|
|
186
|
+
kProviderAuthFields[currentSelectedAuthProvider] || [];
|
|
187
|
+
const currentHasAi =
|
|
188
|
+
currentSelectedProvider === "openai-codex"
|
|
189
|
+
? !!codexStatus.connected
|
|
190
|
+
: currentProviderAuthFields.some((field) =>
|
|
191
|
+
!!String(currentVals[field.key] || "").trim(),
|
|
192
|
+
);
|
|
145
193
|
|
|
146
|
-
|
|
194
|
+
return {
|
|
195
|
+
hasAi: currentHasAi,
|
|
196
|
+
selectedProvider: currentSelectedProvider,
|
|
197
|
+
codexLoading,
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const validationContext = getValidationContext(vals);
|
|
202
|
+
const { selectedProvider, hasAi } = validationContext;
|
|
147
203
|
const placeholderReview = normalizePlaceholderReview(
|
|
148
204
|
vals[kImportPlaceholderReviewKey],
|
|
149
205
|
);
|
|
@@ -164,23 +220,10 @@ export const useWelcome = ({ onComplete }) => {
|
|
|
164
220
|
const canToggleFullCatalog =
|
|
165
221
|
featuredModels.length > 0 && models.length > featuredModels.length;
|
|
166
222
|
const visibleAiFieldKeys = getVisibleAiFieldKeys(selectedProvider);
|
|
167
|
-
const selectedAuthProvider = getAuthProviderFromModelProvider(selectedProvider);
|
|
168
|
-
const selectedProviderAuthFields = kProviderAuthFields[selectedAuthProvider] || [];
|
|
169
|
-
const hasAi =
|
|
170
|
-
selectedProvider === "openai-codex"
|
|
171
|
-
? !!codexStatus.connected
|
|
172
|
-
: selectedProviderAuthFields.some(
|
|
173
|
-
(field) => !!String(vals[field.key] || "").trim(),
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const allValid = kWelcomeGroups.every((group) => group.validate(vals, { hasAi }));
|
|
177
223
|
const isPreStep = step === -1;
|
|
178
224
|
const isSetupStep = step === kSetupStepIndex;
|
|
179
225
|
const isPairingStep = step === kPairingStepIndex;
|
|
180
226
|
const activeGroup = step >= 0 && step < kSetupStepIndex ? kWelcomeGroups[step] : null;
|
|
181
|
-
const currentGroupValid = activeGroup
|
|
182
|
-
? activeGroup.validate(vals, { hasAi })
|
|
183
|
-
: false;
|
|
184
227
|
const selectedPairingChannel = String(
|
|
185
228
|
vals[kPairingChannelKey] || getPreferredPairingChannel(vals),
|
|
186
229
|
);
|
|
@@ -202,7 +245,21 @@ export const useWelcome = ({ onComplete }) => {
|
|
|
202
245
|
const handleSubmit = async () => {
|
|
203
246
|
const { normalizedVals, didChange } = normalizeOnboardingVals(vals);
|
|
204
247
|
if (didChange) setVals(normalizedVals);
|
|
205
|
-
|
|
248
|
+
const submitValidationContext = getValidationContext(normalizedVals);
|
|
249
|
+
const invalidGroup = findFirstInvalidWelcomeGroup(
|
|
250
|
+
normalizedVals,
|
|
251
|
+
submitValidationContext,
|
|
252
|
+
);
|
|
253
|
+
if (invalidGroup) {
|
|
254
|
+
setFormError(
|
|
255
|
+
getWelcomeGroupError(
|
|
256
|
+
invalidGroup.id,
|
|
257
|
+
normalizedVals,
|
|
258
|
+
submitValidationContext,
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
setSetupError(null);
|
|
262
|
+
setStep(kWelcomeGroups.findIndex((group) => group.id === invalidGroup.id));
|
|
206
263
|
return;
|
|
207
264
|
}
|
|
208
265
|
if (loading) return;
|
|
@@ -309,7 +366,17 @@ export const useWelcome = ({ onComplete }) => {
|
|
|
309
366
|
const goNext = async () => {
|
|
310
367
|
const { normalizedVals, didChange } = normalizeOnboardingVals(vals);
|
|
311
368
|
if (didChange) setVals(normalizedVals);
|
|
312
|
-
if (!activeGroup
|
|
369
|
+
if (!activeGroup) return;
|
|
370
|
+
const stepValidationContext = getValidationContext(normalizedVals);
|
|
371
|
+
const stepValidationError = getWelcomeGroupError(
|
|
372
|
+
activeGroup.id,
|
|
373
|
+
normalizedVals,
|
|
374
|
+
stepValidationContext,
|
|
375
|
+
);
|
|
376
|
+
if (stepValidationError) {
|
|
377
|
+
setFormError(stepValidationError);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
313
380
|
setFormError(null);
|
|
314
381
|
if (activeGroup.id === "github") {
|
|
315
382
|
const githubFlow = normalizedVals._GITHUB_FLOW || kGithubFlowFresh;
|
|
@@ -545,12 +612,10 @@ export const useWelcome = ({ onComplete }) => {
|
|
|
545
612
|
canToggleFullCatalog,
|
|
546
613
|
visibleAiFieldKeys,
|
|
547
614
|
hasAi,
|
|
548
|
-
allValid,
|
|
549
615
|
isPreStep,
|
|
550
616
|
isSetupStep,
|
|
551
617
|
isPairingStep,
|
|
552
618
|
activeGroup,
|
|
553
|
-
currentGroupValid,
|
|
554
619
|
selectedPairingChannel,
|
|
555
620
|
placeholderReview,
|
|
556
621
|
isImportStep,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const kCodexAuthStartPath = "/auth/codex/start";
|
|
2
|
+
const kCodexAuthWindowName = "codex-auth";
|
|
3
|
+
const kCodexAuthPopupFeatures = "popup=yes,width=640,height=780";
|
|
4
|
+
const kCodexAuthCallbackMessageType = "callback-input";
|
|
5
|
+
|
|
6
|
+
export const openCodexAuthWindow = () => {
|
|
7
|
+
const popup = window.open(
|
|
8
|
+
kCodexAuthStartPath,
|
|
9
|
+
kCodexAuthWindowName,
|
|
10
|
+
kCodexAuthPopupFeatures,
|
|
11
|
+
);
|
|
12
|
+
if (!popup || popup.closed) {
|
|
13
|
+
window.location.href = kCodexAuthStartPath;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return popup;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const isCodexAuthCallbackMessage = (value) =>
|
|
20
|
+
value?.codex === kCodexAuthCallbackMessageType &&
|
|
21
|
+
typeof value.input === "string" &&
|
|
22
|
+
value.input.trim().length > 0;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getFeaturedModels } from "./model-config.js";
|
|
2
|
+
|
|
3
|
+
export const kModelCatalogCacheKey = "/api/models";
|
|
4
|
+
export const kModelCatalogPollIntervalMs = 3000;
|
|
5
|
+
|
|
6
|
+
export const getModelCatalogModels = (payload) =>
|
|
7
|
+
Array.isArray(payload?.models) ? payload.models : [];
|
|
8
|
+
|
|
9
|
+
export const isModelCatalogRefreshing = (payload) =>
|
|
10
|
+
Boolean(payload?.refreshing);
|
|
11
|
+
|
|
12
|
+
export const getInitialOnboardingModelKey = ({
|
|
13
|
+
catalog = [],
|
|
14
|
+
currentModelKey = "",
|
|
15
|
+
} = {}) => {
|
|
16
|
+
const normalizedCurrent = String(currentModelKey || "").trim();
|
|
17
|
+
if (normalizedCurrent) return normalizedCurrent;
|
|
18
|
+
const featuredModels = getFeaturedModels(catalog);
|
|
19
|
+
return String(featuredModels[0]?.key || catalog[0]?.key || "");
|
|
20
|
+
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
// --- UI settings (single JSON blob containing sub-keys) ---
|
|
8
8
|
export const kUiSettingsStorageKey = "alphaclaw.ui.settings";
|
|
9
|
+
export const kThemeStorageKey = "alphaclaw.ui.theme";
|
|
9
10
|
|
|
10
11
|
// --- Browse / file viewer ---
|
|
11
12
|
export const kFileViewerModeStorageKey = "alphaclaw.browse.viewerMode";
|
|
@@ -30,4 +31,3 @@ export const kAgentLastSessionKey = "alphaclaw.agent.lastSessionKey";
|
|
|
30
31
|
|
|
31
32
|
// --- Chat ---
|
|
32
33
|
export const kChatSessionDraftsStorageKey = "alphaclaw.chat.sessionDrafts";
|
|
33
|
-
|
package/lib/public/login.html
CHANGED
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
<link rel="icon" type="image/svg+xml" href="./img/logo.svg" />
|
|
12
12
|
<link rel="stylesheet" href="./css/theme.css" />
|
|
13
13
|
<link rel="stylesheet" href="./css/tailwind.generated.css" />
|
|
14
|
+
<script>
|
|
15
|
+
try {
|
|
16
|
+
var t = localStorage.getItem("alphaclaw.ui.theme");
|
|
17
|
+
if (t === "system") t = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
18
|
+
else if (t !== "dark" && t !== "light") t = "dark";
|
|
19
|
+
document.documentElement.dataset.theme = t;
|
|
20
|
+
} catch {}
|
|
21
|
+
</script>
|
|
14
22
|
</head>
|
|
15
23
|
<body class="min-h-screen flex items-center justify-center p-4">
|
|
16
24
|
<div class="max-w-sm w-full relative z-10">
|
|
@@ -53,10 +61,6 @@
|
|
|
53
61
|
</form>
|
|
54
62
|
</div>
|
|
55
63
|
<script>
|
|
56
|
-
try {
|
|
57
|
-
window.localStorage?.clear?.();
|
|
58
|
-
} catch {}
|
|
59
|
-
|
|
60
64
|
const formEl = document.getElementById("login-form");
|
|
61
65
|
const passwordEl = document.getElementById("password");
|
|
62
66
|
const submitButtonEl = document.getElementById("submit-btn");
|
package/lib/public/setup.html
CHANGED
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
<link rel="stylesheet" href="./css/agents.css" />
|
|
17
17
|
<link rel="stylesheet" href="./css/chat.css" />
|
|
18
18
|
<link rel="stylesheet" href="./css/cron.css" />
|
|
19
|
+
<script>
|
|
20
|
+
// Apply saved theme before render to prevent flash.
|
|
21
|
+
try {
|
|
22
|
+
var t = localStorage.getItem("alphaclaw.ui.theme");
|
|
23
|
+
if (t === "system") t = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
24
|
+
else if (t !== "dark" && t !== "light") t = "dark";
|
|
25
|
+
document.documentElement.dataset.theme = t;
|
|
26
|
+
} catch {}
|
|
27
|
+
</script>
|
|
19
28
|
</head>
|
|
20
29
|
<body>
|
|
21
30
|
<div id="app"></div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const childProcess = require("child_process");
|
|
2
2
|
const fs = require("fs");
|
|
3
|
+
const os = require("os");
|
|
3
4
|
const path = require("path");
|
|
4
5
|
const https = require("https");
|
|
5
6
|
const http = require("http");
|
|
@@ -7,6 +8,7 @@ const {
|
|
|
7
8
|
kLatestVersionCacheTtlMs,
|
|
8
9
|
kAlphaclawRegistryUrl,
|
|
9
10
|
kNpmPackageRoot,
|
|
11
|
+
kOpenclawUpdateCopyTimeoutMs,
|
|
10
12
|
kRootDir,
|
|
11
13
|
} = require("./constants");
|
|
12
14
|
|
|
@@ -114,7 +116,7 @@ const createAlphaclawVersionService = () => {
|
|
|
114
116
|
const parent = path.dirname(dir);
|
|
115
117
|
if (
|
|
116
118
|
path.basename(parent) === "node_modules" ||
|
|
117
|
-
parent.includes(
|
|
119
|
+
parent.includes(`${path.sep}node_modules${path.sep}`)
|
|
118
120
|
) {
|
|
119
121
|
dir = parent;
|
|
120
122
|
continue;
|
|
@@ -123,7 +125,11 @@ const createAlphaclawVersionService = () => {
|
|
|
123
125
|
if (fs.existsSync(pkgPath)) {
|
|
124
126
|
try {
|
|
125
127
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
126
|
-
if (
|
|
128
|
+
if (
|
|
129
|
+
pkg.dependencies?.["@chrysb/alphaclaw"] ||
|
|
130
|
+
pkg.devDependencies?.["@chrysb/alphaclaw"] ||
|
|
131
|
+
pkg.optionalDependencies?.["@chrysb/alphaclaw"]
|
|
132
|
+
) {
|
|
127
133
|
return parent;
|
|
128
134
|
}
|
|
129
135
|
} catch {}
|
|
@@ -137,19 +143,39 @@ const createAlphaclawVersionService = () => {
|
|
|
137
143
|
const installLatestAlphaclaw = () =>
|
|
138
144
|
new Promise((resolve, reject) => {
|
|
139
145
|
const installDir = findInstallDir();
|
|
146
|
+
const tmpDir = fs.mkdtempSync(
|
|
147
|
+
path.join(os.tmpdir(), "alphaclaw-update-"),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const cleanup = () => {
|
|
151
|
+
try {
|
|
152
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
153
|
+
} catch {}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
fs.writeFileSync(
|
|
157
|
+
path.join(tmpDir, "package.json"),
|
|
158
|
+
JSON.stringify({
|
|
159
|
+
private: true,
|
|
160
|
+
dependencies: { "@chrysb/alphaclaw": "latest" },
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const npmEnv = {
|
|
165
|
+
...process.env,
|
|
166
|
+
npm_config_update_notifier: "false",
|
|
167
|
+
npm_config_fund: "false",
|
|
168
|
+
npm_config_audit: "false",
|
|
169
|
+
};
|
|
170
|
+
|
|
140
171
|
console.log(
|
|
141
|
-
`[alphaclaw] Running: npm install @chrysb/alphaclaw@latest (
|
|
172
|
+
`[alphaclaw] Running: npm install @chrysb/alphaclaw@latest in temp dir (target: ${installDir})`,
|
|
142
173
|
);
|
|
143
174
|
childProcess.exec(
|
|
144
|
-
"npm install
|
|
175
|
+
"npm install --omit=dev --prefer-online --package-lock=false",
|
|
145
176
|
{
|
|
146
|
-
cwd:
|
|
147
|
-
env:
|
|
148
|
-
...process.env,
|
|
149
|
-
npm_config_update_notifier: "false",
|
|
150
|
-
npm_config_fund: "false",
|
|
151
|
-
npm_config_audit: "false",
|
|
152
|
-
},
|
|
177
|
+
cwd: tmpDir,
|
|
178
|
+
env: npmEnv,
|
|
153
179
|
timeout: 180000,
|
|
154
180
|
},
|
|
155
181
|
(err, stdout, stderr) => {
|
|
@@ -158,6 +184,7 @@ const createAlphaclawVersionService = () => {
|
|
|
158
184
|
console.log(
|
|
159
185
|
`[alphaclaw] alphaclaw install error: ${message.slice(0, 200)}`,
|
|
160
186
|
);
|
|
187
|
+
cleanup();
|
|
161
188
|
return reject(
|
|
162
189
|
new Error(
|
|
163
190
|
message || "Failed to install @chrysb/alphaclaw@latest",
|
|
@@ -169,8 +196,28 @@ const createAlphaclawVersionService = () => {
|
|
|
169
196
|
`[alphaclaw] alphaclaw install stdout: ${stdout.trim().slice(0, 300)}`,
|
|
170
197
|
);
|
|
171
198
|
}
|
|
172
|
-
|
|
173
|
-
|
|
199
|
+
|
|
200
|
+
const src = path.join(tmpDir, "node_modules");
|
|
201
|
+
const dest = path.join(installDir, "node_modules");
|
|
202
|
+
childProcess.exec(
|
|
203
|
+
`cp -af "${src}/." "${dest}/"`,
|
|
204
|
+
{ timeout: kOpenclawUpdateCopyTimeoutMs },
|
|
205
|
+
(copyErr) => {
|
|
206
|
+
cleanup();
|
|
207
|
+
if (copyErr) {
|
|
208
|
+
console.log(
|
|
209
|
+
`[alphaclaw] alphaclaw copy error: ${(copyErr.message || "").slice(0, 200)}`,
|
|
210
|
+
);
|
|
211
|
+
return reject(
|
|
212
|
+
new Error(
|
|
213
|
+
`Failed to copy updated AlphaClaw files: ${copyErr.message}`,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
console.log("[alphaclaw] alphaclaw install completed");
|
|
218
|
+
resolve({ stdout: stdout?.trim(), stderr: stderr?.trim() });
|
|
219
|
+
},
|
|
220
|
+
);
|
|
174
221
|
},
|
|
175
222
|
);
|
|
176
223
|
});
|
|
@@ -10,6 +10,7 @@ let pruneTimer = null;
|
|
|
10
10
|
const kDefaultRequestLimit = 50;
|
|
11
11
|
const kMaxRequestLimit = 200;
|
|
12
12
|
const kPruneIntervalMs = 12 * 60 * 60 * 1000;
|
|
13
|
+
const kHealthSummaryWindow = 25;
|
|
13
14
|
|
|
14
15
|
const ensureDb = () => {
|
|
15
16
|
if (!db) throw new Error("Webhooks DB not initialized");
|
|
@@ -202,22 +203,61 @@ const getHookSummaries = () => {
|
|
|
202
203
|
const database = ensureDb();
|
|
203
204
|
const rows = database
|
|
204
205
|
.prepare(`
|
|
206
|
+
WITH ranked_requests AS (
|
|
207
|
+
SELECT
|
|
208
|
+
hook_name,
|
|
209
|
+
created_at,
|
|
210
|
+
gateway_status,
|
|
211
|
+
ROW_NUMBER() OVER (
|
|
212
|
+
PARTITION BY hook_name
|
|
213
|
+
ORDER BY created_at DESC, id DESC
|
|
214
|
+
) AS row_num
|
|
215
|
+
FROM webhook_requests
|
|
216
|
+
),
|
|
217
|
+
overall_counts AS (
|
|
218
|
+
SELECT
|
|
219
|
+
hook_name,
|
|
220
|
+
MAX(created_at) AS last_received,
|
|
221
|
+
COUNT(*) AS total_count,
|
|
222
|
+
SUM(CASE WHEN gateway_status >= 200 AND gateway_status < 300 THEN 1 ELSE 0 END) AS success_count,
|
|
223
|
+
SUM(CASE WHEN gateway_status IS NULL OR gateway_status < 200 OR gateway_status >= 300 THEN 1 ELSE 0 END) AS error_count
|
|
224
|
+
FROM webhook_requests
|
|
225
|
+
GROUP BY hook_name
|
|
226
|
+
),
|
|
227
|
+
recent_counts AS (
|
|
228
|
+
SELECT
|
|
229
|
+
hook_name,
|
|
230
|
+
COUNT(*) AS recent_total_count,
|
|
231
|
+
SUM(CASE WHEN gateway_status >= 200 AND gateway_status < 300 THEN 1 ELSE 0 END) AS recent_success_count,
|
|
232
|
+
SUM(CASE WHEN gateway_status IS NULL OR gateway_status < 200 OR gateway_status >= 300 THEN 1 ELSE 0 END) AS recent_error_count
|
|
233
|
+
FROM ranked_requests
|
|
234
|
+
WHERE row_num <= $health_window
|
|
235
|
+
GROUP BY hook_name
|
|
236
|
+
)
|
|
205
237
|
SELECT
|
|
206
|
-
hook_name,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
238
|
+
overall_counts.hook_name,
|
|
239
|
+
overall_counts.last_received,
|
|
240
|
+
overall_counts.total_count,
|
|
241
|
+
overall_counts.success_count,
|
|
242
|
+
overall_counts.error_count,
|
|
243
|
+
COALESCE(recent_counts.recent_total_count, 0) AS recent_total_count,
|
|
244
|
+
COALESCE(recent_counts.recent_success_count, 0) AS recent_success_count,
|
|
245
|
+
COALESCE(recent_counts.recent_error_count, 0) AS recent_error_count
|
|
246
|
+
FROM overall_counts
|
|
247
|
+
LEFT JOIN recent_counts
|
|
248
|
+
ON recent_counts.hook_name = overall_counts.hook_name
|
|
213
249
|
`)
|
|
214
|
-
.all();
|
|
250
|
+
.all({ $health_window: kHealthSummaryWindow });
|
|
215
251
|
return rows.map((row) => ({
|
|
216
252
|
hookName: row.hook_name,
|
|
217
253
|
lastReceived: row.last_received || null,
|
|
218
254
|
totalCount: Number(row.total_count || 0),
|
|
219
255
|
successCount: Number(row.success_count || 0),
|
|
220
256
|
errorCount: Number(row.error_count || 0),
|
|
257
|
+
recentTotalCount: Number(row.recent_total_count || 0),
|
|
258
|
+
recentSuccessCount: Number(row.recent_success_count || 0),
|
|
259
|
+
recentErrorCount: Number(row.recent_error_count || 0),
|
|
260
|
+
healthWindowSize: kHealthSummaryWindow,
|
|
221
261
|
}));
|
|
222
262
|
};
|
|
223
263
|
|