@chrysb/alphaclaw 0.1.25 → 0.2.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/bin/alphaclaw.js +400 -54
- package/lib/public/js/app.js +38 -19
- package/lib/public/js/components/channels.js +14 -3
- package/lib/public/js/components/onboarding/welcome-form-step.js +5 -2
- package/lib/public/js/components/onboarding/welcome-setup-step.js +2 -2
- package/lib/public/js/components/telegram-workspace.js +1377 -0
- package/lib/public/js/components/welcome.js +23 -1
- package/lib/public/js/lib/api.js +9 -0
- package/lib/server/constants.js +1 -0
- package/lib/server/helpers.js +32 -0
- package/lib/server/onboarding/github.js +96 -31
- package/lib/server/onboarding/index.js +86 -33
- package/lib/server/onboarding/openclaw.js +4 -11
- package/lib/server/onboarding/workspace.js +26 -3
- package/lib/server/routes/auth.js +24 -6
- package/lib/server/routes/onboarding.js +36 -0
- package/lib/server/routes/proxy.js +4 -4
- package/lib/server/routes/telegram.js +407 -0
- package/lib/server/telegram-api.js +65 -0
- package/lib/server/telegram-workspace.js +82 -0
- package/lib/server/topic-registry.js +152 -0
- package/lib/server.js +7 -1
- package/lib/setup/core-prompts/TOOLS.md +6 -6
- package/lib/setup/env.template +3 -0
- package/lib/setup/gitignore +2 -0
- package/lib/setup/hourly-git-sync.sh +9 -10
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from "https://esm.sh/preact/hooks";
|
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import {
|
|
5
5
|
runOnboard,
|
|
6
|
+
verifyGithubOnboardingRepo,
|
|
6
7
|
fetchModels,
|
|
7
8
|
fetchCodexStatus,
|
|
8
9
|
disconnectCodex,
|
|
@@ -58,6 +59,7 @@ export const Welcome = ({ onComplete }) => {
|
|
|
58
59
|
const [codexAuthStarted, setCodexAuthStarted] = useState(false);
|
|
59
60
|
const [codexAuthWaiting, setCodexAuthWaiting] = useState(false);
|
|
60
61
|
const [loading, setLoading] = useState(false);
|
|
62
|
+
const [githubStepLoading, setGithubStepLoading] = useState(false);
|
|
61
63
|
const [error, setError] = useState(null);
|
|
62
64
|
const codexPopupPollRef = useRef(null);
|
|
63
65
|
|
|
@@ -360,8 +362,27 @@ export const Welcome = ({ onComplete }) => {
|
|
|
360
362
|
setStep(kWelcomeGroups.length - 1);
|
|
361
363
|
};
|
|
362
364
|
|
|
363
|
-
const goNext = () => {
|
|
365
|
+
const goNext = async () => {
|
|
364
366
|
if (!activeGroup || !currentGroupValid) return;
|
|
367
|
+
if (activeGroup.id === "github") {
|
|
368
|
+
setGithubStepLoading(true);
|
|
369
|
+
setError(null);
|
|
370
|
+
try {
|
|
371
|
+
const result = await verifyGithubOnboardingRepo(
|
|
372
|
+
vals.GITHUB_WORKSPACE_REPO,
|
|
373
|
+
vals.GITHUB_TOKEN,
|
|
374
|
+
);
|
|
375
|
+
if (!result?.ok) {
|
|
376
|
+
setError(result?.error || "GitHub verification failed");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
setError(err?.message || "GitHub verification failed");
|
|
381
|
+
return;
|
|
382
|
+
} finally {
|
|
383
|
+
setGithubStepLoading(false);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
365
386
|
setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
|
|
366
387
|
};
|
|
367
388
|
|
|
@@ -438,6 +459,7 @@ export const Welcome = ({ onComplete }) => {
|
|
|
438
459
|
goBack=${goBack}
|
|
439
460
|
goNext=${goNext}
|
|
440
461
|
loading=${loading}
|
|
462
|
+
githubStepLoading=${githubStepLoading}
|
|
441
463
|
allValid=${allValid}
|
|
442
464
|
handleSubmit=${handleSubmit}
|
|
443
465
|
/>
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -167,6 +167,15 @@ export async function runOnboard(vars, modelKey) {
|
|
|
167
167
|
return res.json();
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
export async function verifyGithubOnboardingRepo(repo, token) {
|
|
171
|
+
const res = await authFetch('/api/onboard/github/verify', {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({ repo, token }),
|
|
175
|
+
});
|
|
176
|
+
return res.json();
|
|
177
|
+
}
|
|
178
|
+
|
|
170
179
|
export const fetchModels = async () => {
|
|
171
180
|
const res = await authFetch('/api/models');
|
|
172
181
|
return res.json();
|
package/lib/server/constants.js
CHANGED
package/lib/server/helpers.js
CHANGED
|
@@ -63,6 +63,8 @@ const normalizeIp = (ip) => String(ip || "").replace(/^::ffff:/, "");
|
|
|
63
63
|
|
|
64
64
|
const isTruthyEnvFlag = (value) =>
|
|
65
65
|
["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
|
|
66
|
+
const isDebugEnabled = () =>
|
|
67
|
+
isTruthyEnvFlag(process.env.ALPHACLAW_DEBUG) || isTruthyEnvFlag(process.env.DEBUG);
|
|
66
68
|
|
|
67
69
|
const getClientKey = (req) =>
|
|
68
70
|
normalizeIp(
|
|
@@ -172,6 +174,33 @@ const readGoogleCredentials = () => {
|
|
|
172
174
|
}
|
|
173
175
|
};
|
|
174
176
|
|
|
177
|
+
const kSecretKeyMatchers = [
|
|
178
|
+
/(?:^|_)TOKEN(?:$|_)/i,
|
|
179
|
+
/(?:^|_)API_KEY(?:$|_)/i,
|
|
180
|
+
/(?:^|_)PASSWORD(?:$|_)/i,
|
|
181
|
+
/(?:^|_)SECRET(?:$|_)/i,
|
|
182
|
+
/(?:^|_)PRIVATE_KEY(?:$|_)/i,
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const isSensitiveKey = (key) =>
|
|
186
|
+
kSecretKeyMatchers.some((matcher) => matcher.test(String(key || "")));
|
|
187
|
+
|
|
188
|
+
const buildSecretReplacements = (...sources) => {
|
|
189
|
+
const replacements = [];
|
|
190
|
+
const seen = new Set();
|
|
191
|
+
for (const source of sources) {
|
|
192
|
+
for (const [rawKey, rawValue] of Object.entries(source || {})) {
|
|
193
|
+
const key = String(rawKey || "").trim();
|
|
194
|
+
const value = String(rawValue || "");
|
|
195
|
+
if (!key || !value || !isSensitiveKey(key)) continue;
|
|
196
|
+
if (seen.has(value)) continue;
|
|
197
|
+
seen.add(value);
|
|
198
|
+
replacements.push([value, `\${${key}}`]);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return replacements.sort((a, b) => b[0].length - a[0].length);
|
|
202
|
+
};
|
|
203
|
+
|
|
175
204
|
module.exports = {
|
|
176
205
|
normalizeOpenclawVersion,
|
|
177
206
|
compareVersionParts,
|
|
@@ -180,6 +209,7 @@ module.exports = {
|
|
|
180
209
|
getCodexAccountId,
|
|
181
210
|
normalizeIp,
|
|
182
211
|
isTruthyEnvFlag,
|
|
212
|
+
isDebugEnabled,
|
|
183
213
|
getClientKey,
|
|
184
214
|
resolveGithubRepoUrl,
|
|
185
215
|
createPkcePair,
|
|
@@ -189,4 +219,6 @@ module.exports = {
|
|
|
189
219
|
getBaseUrl,
|
|
190
220
|
getApiEnableUrl,
|
|
191
221
|
readGoogleCredentials,
|
|
222
|
+
isSensitiveKey,
|
|
223
|
+
buildSecretReplacements,
|
|
192
224
|
};
|
|
@@ -1,49 +1,114 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Accept: "application/vnd.github+json",
|
|
7
|
-
};
|
|
1
|
+
const buildGithubHeaders = (githubToken) => ({
|
|
2
|
+
Authorization: `token ${githubToken}`,
|
|
3
|
+
"User-Agent": "openclaw-railway",
|
|
4
|
+
Accept: "application/vnd.github+json",
|
|
5
|
+
});
|
|
8
6
|
|
|
7
|
+
const parseGithubErrorMessage = async (response) => {
|
|
9
8
|
try {
|
|
10
|
-
const
|
|
9
|
+
const payload = await response.json();
|
|
10
|
+
if (typeof payload?.message === "string" && payload.message.trim()) {
|
|
11
|
+
return payload.message.trim();
|
|
12
|
+
}
|
|
13
|
+
} catch {}
|
|
14
|
+
return response.statusText || `HTTP ${response.status}`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const verifyGithubRepoForOnboarding = async ({ repoUrl, githubToken }) => {
|
|
18
|
+
const ghHeaders = buildGithubHeaders(githubToken);
|
|
19
|
+
const [repoOwner] = String(repoUrl || "").split("/", 1);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const userRes = await fetch("https://api.github.com/user", {
|
|
11
23
|
headers: ghHeaders,
|
|
12
24
|
});
|
|
25
|
+
if (!userRes.ok) {
|
|
26
|
+
const details = await parseGithubErrorMessage(userRes);
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
status: 400,
|
|
30
|
+
error: `Cannot verify GitHub token: ${details}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const authedUser = await userRes.json().catch(() => ({}));
|
|
34
|
+
const authedLogin = String(authedUser?.login || "").trim();
|
|
35
|
+
if (
|
|
36
|
+
repoOwner &&
|
|
37
|
+
authedLogin &&
|
|
38
|
+
repoOwner.toLowerCase() !== authedLogin.toLowerCase()
|
|
39
|
+
) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
status: 400,
|
|
43
|
+
error: `Workspace repo owner must match your token user "${authedLogin}"`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
13
46
|
|
|
47
|
+
const checkRes = await fetch(`https://api.github.com/repos/${repoUrl}`, {
|
|
48
|
+
headers: ghHeaders,
|
|
49
|
+
});
|
|
14
50
|
if (checkRes.status === 404) {
|
|
15
|
-
console.log(`[onboard] Creating repo ${repoUrl}...`);
|
|
16
|
-
const createRes = await fetch("https://api.github.com/user/repos", {
|
|
17
|
-
method: "POST",
|
|
18
|
-
headers: { ...ghHeaders, "Content-Type": "application/json" },
|
|
19
|
-
body: JSON.stringify({
|
|
20
|
-
name: repoName,
|
|
21
|
-
private: true,
|
|
22
|
-
auto_init: false,
|
|
23
|
-
}),
|
|
24
|
-
});
|
|
25
|
-
if (!createRes.ok) {
|
|
26
|
-
const err = await createRes.json().catch(() => ({}));
|
|
27
|
-
return {
|
|
28
|
-
ok: false,
|
|
29
|
-
status: 400,
|
|
30
|
-
error: `Failed to create repo: ${err.message || createRes.statusText}`,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
console.log(`[onboard] Repo ${repoUrl} created`);
|
|
34
51
|
return { ok: true };
|
|
35
52
|
}
|
|
53
|
+
if (checkRes.ok) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
status: 400,
|
|
57
|
+
error: `Repository "${repoUrl}" already exists.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
36
60
|
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
const details = await parseGithubErrorMessage(checkRes);
|
|
39
62
|
return {
|
|
40
63
|
ok: false,
|
|
41
64
|
status: 400,
|
|
42
|
-
error: `Cannot
|
|
65
|
+
error: `Cannot verify repo "${repoUrl}": ${details}`,
|
|
43
66
|
};
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
status: 400,
|
|
71
|
+
error: `GitHub verification error: ${e.message}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const ensureGithubRepoAccessible = async ({
|
|
77
|
+
repoUrl,
|
|
78
|
+
repoName,
|
|
79
|
+
githubToken,
|
|
80
|
+
}) => {
|
|
81
|
+
const ghHeaders = buildGithubHeaders(githubToken);
|
|
82
|
+
const verification = await verifyGithubRepoForOnboarding({
|
|
83
|
+
repoUrl,
|
|
84
|
+
githubToken,
|
|
85
|
+
});
|
|
86
|
+
if (!verification.ok) return verification;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
console.log(`[onboard] Creating repo ${repoUrl}...`);
|
|
90
|
+
const createRes = await fetch("https://api.github.com/user/repos", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { ...ghHeaders, "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
name: repoName,
|
|
95
|
+
private: true,
|
|
96
|
+
auto_init: false,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
if (!createRes.ok) {
|
|
100
|
+
const details = await parseGithubErrorMessage(createRes);
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
status: 400,
|
|
104
|
+
error: `Failed to create repo: ${details}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
console.log(`[onboard] Repo ${repoUrl} created`);
|
|
108
|
+
return { ok: true };
|
|
44
109
|
} catch (e) {
|
|
45
110
|
return { ok: false, status: 400, error: `GitHub error: ${e.message}` };
|
|
46
111
|
}
|
|
47
112
|
};
|
|
48
113
|
|
|
49
|
-
module.exports = { ensureGithubRepoAccessible };
|
|
114
|
+
module.exports = { ensureGithubRepoAccessible, verifyGithubRepoForOnboarding };
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const { kSetupDir, kRootDir } = require("../constants");
|
|
3
3
|
const { validateOnboardingInput } = require("./validation");
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
ensureGithubRepoAccessible,
|
|
6
|
+
verifyGithubRepoForOnboarding,
|
|
7
|
+
} = require("./github");
|
|
8
|
+
const {
|
|
9
|
+
buildOnboardArgs,
|
|
10
|
+
writeSanitizedOpenclawConfig,
|
|
11
|
+
} = require("./openclaw");
|
|
12
|
+
const {
|
|
13
|
+
installControlUiSkill,
|
|
14
|
+
syncBootstrapPromptFiles,
|
|
15
|
+
} = require("./workspace");
|
|
16
|
+
const {
|
|
17
|
+
installHourlyGitSyncScript,
|
|
18
|
+
installHourlyGitSyncCron,
|
|
19
|
+
} = require("./cron");
|
|
9
20
|
|
|
10
21
|
const createOnboardingService = ({
|
|
11
22
|
fs,
|
|
@@ -23,6 +34,15 @@ const createOnboardingService = ({
|
|
|
23
34
|
}) => {
|
|
24
35
|
const { OPENCLAW_DIR, WORKSPACE_DIR } = constants;
|
|
25
36
|
|
|
37
|
+
const verifyGithubSetup = async ({
|
|
38
|
+
githubRepoInput,
|
|
39
|
+
githubToken,
|
|
40
|
+
resolveGithubRepoUrl,
|
|
41
|
+
}) => {
|
|
42
|
+
const repoUrl = resolveGithubRepoUrl(githubRepoInput);
|
|
43
|
+
return verifyGithubRepoForOnboarding({ repoUrl, githubToken });
|
|
44
|
+
};
|
|
45
|
+
|
|
26
46
|
const completeOnboarding = async ({ req, vars, modelKey }) => {
|
|
27
47
|
const validation = validateOnboardingInput({
|
|
28
48
|
vars,
|
|
@@ -31,43 +51,62 @@ const createOnboardingService = ({
|
|
|
31
51
|
hasCodexOauthProfile,
|
|
32
52
|
});
|
|
33
53
|
if (!validation.ok) {
|
|
34
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
status: validation.status,
|
|
56
|
+
body: { ok: false, error: validation.error },
|
|
57
|
+
};
|
|
35
58
|
}
|
|
36
59
|
|
|
37
|
-
const {
|
|
38
|
-
|
|
60
|
+
const {
|
|
61
|
+
varMap,
|
|
62
|
+
githubToken,
|
|
63
|
+
githubRepoInput,
|
|
64
|
+
selectedProvider,
|
|
65
|
+
hasCodexOauth,
|
|
66
|
+
} = validation.data;
|
|
39
67
|
|
|
40
68
|
const repoUrl = resolveGithubRepoUrl(githubRepoInput);
|
|
41
|
-
const varsToSave = [
|
|
69
|
+
const varsToSave = [
|
|
70
|
+
...vars.filter((v) => v.value && v.key !== "GITHUB_WORKSPACE_REPO"),
|
|
71
|
+
];
|
|
42
72
|
varsToSave.push({ key: "GITHUB_WORKSPACE_REPO", value: repoUrl });
|
|
43
73
|
writeEnvFile(varsToSave);
|
|
44
74
|
reloadEnv();
|
|
45
75
|
|
|
46
|
-
const remoteUrl = `https
|
|
76
|
+
const remoteUrl = `https://github.com/${repoUrl}.git`;
|
|
47
77
|
const [, repoName] = repoUrl.split("/");
|
|
48
78
|
const repoCheck = await ensureGithubRepoAccessible({
|
|
49
79
|
repoUrl,
|
|
50
80
|
repoName,
|
|
51
|
-
remoteUrl,
|
|
52
81
|
githubToken,
|
|
53
82
|
});
|
|
54
83
|
if (!repoCheck.ok) {
|
|
55
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
status: repoCheck.status,
|
|
86
|
+
body: { ok: false, error: repoCheck.error },
|
|
87
|
+
};
|
|
56
88
|
}
|
|
57
89
|
|
|
58
90
|
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
|
59
91
|
fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
|
|
60
|
-
syncBootstrapPromptFiles({
|
|
92
|
+
syncBootstrapPromptFiles({
|
|
93
|
+
fs,
|
|
94
|
+
workspaceDir: WORKSPACE_DIR,
|
|
95
|
+
baseUrl: getBaseUrl(req),
|
|
96
|
+
});
|
|
61
97
|
|
|
62
98
|
if (!fs.existsSync(`${OPENCLAW_DIR}/.git`)) {
|
|
63
99
|
await shellCmd(
|
|
64
|
-
`cd ${OPENCLAW_DIR} && git init -b main && git remote add origin "${remoteUrl}" && git config user.email "agent@
|
|
100
|
+
`cd ${OPENCLAW_DIR} && git init -b main && git remote add origin "${remoteUrl}" && git config user.email "agent@alphaclaw.md" && git config user.name "AlphaClaw Agent"`,
|
|
65
101
|
);
|
|
66
102
|
console.log("[onboard] Git initialized");
|
|
67
103
|
}
|
|
68
104
|
|
|
69
105
|
if (!fs.existsSync(`${OPENCLAW_DIR}/.gitignore`)) {
|
|
70
|
-
fs.copyFileSync(
|
|
106
|
+
fs.copyFileSync(
|
|
107
|
+
path.join(kSetupDir, "gitignore"),
|
|
108
|
+
`${OPENCLAW_DIR}/.gitignore`,
|
|
109
|
+
);
|
|
71
110
|
}
|
|
72
111
|
|
|
73
112
|
const onboardArgs = buildOnboardArgs({
|
|
@@ -76,14 +115,17 @@ const createOnboardingService = ({
|
|
|
76
115
|
hasCodexOauth,
|
|
77
116
|
workspaceDir: WORKSPACE_DIR,
|
|
78
117
|
});
|
|
79
|
-
await shellCmd(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
118
|
+
await shellCmd(
|
|
119
|
+
`openclaw onboard ${onboardArgs.map((a) => `"${a}"`).join(" ")}`,
|
|
120
|
+
{
|
|
121
|
+
env: {
|
|
122
|
+
...process.env,
|
|
123
|
+
OPENCLAW_HOME: kRootDir,
|
|
124
|
+
OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
|
|
125
|
+
},
|
|
126
|
+
timeout: 120000,
|
|
84
127
|
},
|
|
85
|
-
|
|
86
|
-
});
|
|
128
|
+
);
|
|
87
129
|
console.log("[onboard] Onboard complete");
|
|
88
130
|
|
|
89
131
|
await shellCmd(`openclaw models set "${modelKey}"`, {
|
|
@@ -91,7 +133,9 @@ const createOnboardingService = ({
|
|
|
91
133
|
timeout: 30000,
|
|
92
134
|
}).catch((e) => {
|
|
93
135
|
console.error("[onboard] Failed to set model:", e.message);
|
|
94
|
-
throw new Error(
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Onboarding completed but failed to set model "${modelKey}"`,
|
|
138
|
+
);
|
|
95
139
|
});
|
|
96
140
|
|
|
97
141
|
try {
|
|
@@ -101,24 +145,33 @@ const createOnboardingService = ({
|
|
|
101
145
|
writeSanitizedOpenclawConfig({ fs, openclawDir: OPENCLAW_DIR, varMap });
|
|
102
146
|
ensureGatewayProxyConfig(getBaseUrl(req));
|
|
103
147
|
|
|
104
|
-
installControlUiSkill({
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
`cd ${OPENCLAW_DIR} && git add -A && git commit -m "initial setup" && git push ${pushArgs} origin main`,
|
|
110
|
-
{ timeout: 30000 },
|
|
111
|
-
).catch((e) => console.error("[onboard] Git push error:", e.message));
|
|
112
|
-
console.log("[onboard] Initial state committed and pushed");
|
|
148
|
+
installControlUiSkill({
|
|
149
|
+
fs,
|
|
150
|
+
openclawDir: OPENCLAW_DIR,
|
|
151
|
+
baseUrl: getBaseUrl(req),
|
|
152
|
+
});
|
|
113
153
|
|
|
114
154
|
installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
|
|
115
155
|
await installHourlyGitSyncCron({ fs, openclawDir: OPENCLAW_DIR });
|
|
116
156
|
|
|
157
|
+
try {
|
|
158
|
+
await shellCmd(`alphaclaw git-sync -m "initial setup"`, {
|
|
159
|
+
timeout: 30000,
|
|
160
|
+
env: {
|
|
161
|
+
...process.env,
|
|
162
|
+
GITHUB_TOKEN: githubToken,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
console.log("[onboard] Initial state committed and pushed");
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error("[onboard] Git push error:", e.message);
|
|
168
|
+
}
|
|
169
|
+
|
|
117
170
|
startGateway();
|
|
118
171
|
return { status: 200, body: { ok: true } };
|
|
119
172
|
};
|
|
120
173
|
|
|
121
|
-
return { completeOnboarding };
|
|
174
|
+
return { completeOnboarding, verifyGithubSetup };
|
|
122
175
|
};
|
|
123
176
|
|
|
124
177
|
module.exports = { createOnboardingService };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { buildSecretReplacements } = require("../helpers");
|
|
2
|
+
|
|
1
3
|
const buildOnboardArgs = ({ varMap, selectedProvider, hasCodexOauth, workspaceDir }) => {
|
|
2
4
|
const onboardArgs = [
|
|
3
5
|
"--non-interactive",
|
|
@@ -149,18 +151,9 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
|
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
let content = JSON.stringify(cfg, null, 2);
|
|
152
|
-
const replacements =
|
|
153
|
-
[process.env.OPENCLAW_GATEWAY_TOKEN, "${OPENCLAW_GATEWAY_TOKEN}"],
|
|
154
|
-
[varMap.ANTHROPIC_API_KEY, "${ANTHROPIC_API_KEY}"],
|
|
155
|
-
[varMap.ANTHROPIC_TOKEN, "${ANTHROPIC_TOKEN}"],
|
|
156
|
-
[varMap.TELEGRAM_BOT_TOKEN, "${TELEGRAM_BOT_TOKEN}"],
|
|
157
|
-
[varMap.DISCORD_BOT_TOKEN, "${DISCORD_BOT_TOKEN}"],
|
|
158
|
-
[varMap.OPENAI_API_KEY, "${OPENAI_API_KEY}"],
|
|
159
|
-
[varMap.GEMINI_API_KEY, "${GEMINI_API_KEY}"],
|
|
160
|
-
[varMap.BRAVE_API_KEY, "${BRAVE_API_KEY}"],
|
|
161
|
-
];
|
|
154
|
+
const replacements = buildSecretReplacements(varMap, process.env);
|
|
162
155
|
for (const [secret, envRef] of replacements) {
|
|
163
|
-
if (secret
|
|
156
|
+
if (secret) {
|
|
164
157
|
content = content.split(secret).join(envRef);
|
|
165
158
|
}
|
|
166
159
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
|
-
const { kSetupDir } = require("../constants");
|
|
2
|
+
const { kSetupDir, OPENCLAW_DIR } = require("../constants");
|
|
3
|
+
const { renderTopicRegistryMarkdown } = require("../topic-registry");
|
|
3
4
|
|
|
4
5
|
const resolveSetupUiUrl = (baseUrl) => {
|
|
5
6
|
const normalizedBaseUrl = String(baseUrl || "").trim().replace(/\/+$/, "");
|
|
@@ -19,16 +20,38 @@ const resolveSetupUiUrl = (baseUrl) => {
|
|
|
19
20
|
return "http://localhost:3000";
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
// Single assembly point for TOOLS.md: template + topic registry.
|
|
24
|
+
// Idempotent — always rebuilds from source so deploys never clobber topic data.
|
|
25
|
+
const isTelegramWorkspaceEnabled = (fs) => {
|
|
26
|
+
try {
|
|
27
|
+
const configPath = `${OPENCLAW_DIR}/openclaw.json`;
|
|
28
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
29
|
+
return Object.keys(cfg.channels?.telegram?.groups || {}).length > 0;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
22
35
|
const syncBootstrapPromptFiles = ({ fs, workspaceDir, baseUrl }) => {
|
|
23
36
|
try {
|
|
37
|
+
const setupUiUrl = resolveSetupUiUrl(baseUrl);
|
|
24
38
|
const bootstrapDir = `${workspaceDir}/hooks/bootstrap`;
|
|
25
39
|
fs.mkdirSync(bootstrapDir, { recursive: true });
|
|
26
40
|
fs.copyFileSync(path.join(kSetupDir, "core-prompts", "AGENTS.md"), `${bootstrapDir}/AGENTS.md`);
|
|
41
|
+
|
|
27
42
|
const toolsTemplate = fs.readFileSync(path.join(kSetupDir, "core-prompts", "TOOLS.md"), "utf8");
|
|
28
|
-
|
|
43
|
+
let toolsContent = toolsTemplate.replace(
|
|
29
44
|
/\{\{SETUP_UI_URL\}\}/g,
|
|
30
|
-
|
|
45
|
+
setupUiUrl,
|
|
31
46
|
);
|
|
47
|
+
|
|
48
|
+
const topicSection = renderTopicRegistryMarkdown({
|
|
49
|
+
includeSyncGuidance: isTelegramWorkspaceEnabled(fs),
|
|
50
|
+
});
|
|
51
|
+
if (topicSection) {
|
|
52
|
+
toolsContent += topicSection;
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
fs.writeFileSync(`${bootstrapDir}/TOOLS.md`, toolsContent);
|
|
33
56
|
console.log("[onboard] Bootstrap prompt files synced");
|
|
34
57
|
} catch (e) {
|
|
@@ -2,7 +2,8 @@ const crypto = require("crypto");
|
|
|
2
2
|
const { kLoginCleanupIntervalMs } = require("../constants");
|
|
3
3
|
|
|
4
4
|
const registerAuthRoutes = ({ app, loginThrottle }) => {
|
|
5
|
-
const SETUP_PASSWORD = process.env.SETUP_PASSWORD || "";
|
|
5
|
+
const SETUP_PASSWORD = String(process.env.SETUP_PASSWORD || "").trim();
|
|
6
|
+
const kAuthMisconfigured = !SETUP_PASSWORD;
|
|
6
7
|
const kSessionTtlMs = 7 * 24 * 60 * 60 * 1000;
|
|
7
8
|
|
|
8
9
|
const signSessionPayload = (payload) =>
|
|
@@ -50,7 +51,13 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
|
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
app.post("/api/auth/login", (req, res) => {
|
|
53
|
-
if (
|
|
54
|
+
if (kAuthMisconfigured) {
|
|
55
|
+
return res.status(503).json({
|
|
56
|
+
ok: false,
|
|
57
|
+
error:
|
|
58
|
+
"Server misconfigured: SETUP_PASSWORD is missing. Set it in your deployment environment variables and restart.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
54
61
|
const now = Date.now();
|
|
55
62
|
const clientKey = loginThrottle.getClientKey(req);
|
|
56
63
|
const state = loginThrottle.getOrCreateLoginAttemptState(clientKey, now);
|
|
@@ -92,18 +99,29 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
|
|
|
92
99
|
}, kLoginCleanupIntervalMs).unref();
|
|
93
100
|
|
|
94
101
|
const isAuthorizedRequest = (req) => {
|
|
95
|
-
if (
|
|
102
|
+
if (kAuthMisconfigured) return false;
|
|
96
103
|
const requestPath = req.path || "";
|
|
97
104
|
if (requestPath.startsWith("/auth/google/callback")) return true;
|
|
98
105
|
if (requestPath.startsWith("/auth/codex/callback")) return true;
|
|
99
106
|
const cookies = cookieParser(req);
|
|
100
|
-
const
|
|
101
|
-
const token = cookies.setup_token || query.token;
|
|
107
|
+
const token = cookies.setup_token;
|
|
102
108
|
return verifySessionToken(token);
|
|
103
109
|
};
|
|
104
110
|
|
|
105
111
|
const requireAuth = (req, res, next) => {
|
|
106
|
-
if (
|
|
112
|
+
if (kAuthMisconfigured) {
|
|
113
|
+
if (req.path.startsWith("/api/")) {
|
|
114
|
+
return res.status(503).json({
|
|
115
|
+
error:
|
|
116
|
+
"Server misconfigured: SETUP_PASSWORD is missing. Set it in your deployment environment variables and restart.",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return res
|
|
120
|
+
.status(503)
|
|
121
|
+
.send(
|
|
122
|
+
"Setup auth is not configured. Set SETUP_PASSWORD in your deployment environment and restart.",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
107
125
|
if (req.path.startsWith("/auth/google/callback")) return next();
|
|
108
126
|
if (req.path.startsWith("/auth/codex/callback")) return next();
|
|
109
127
|
if (isAuthorizedRequest(req)) return next();
|
|
@@ -111,6 +111,42 @@ const registerOnboardingRoutes = ({
|
|
|
111
111
|
res.status(500).json({ ok: false, error: sanitizeOnboardingError(err) });
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
app.post("/api/onboard/github/verify", async (req, res) => {
|
|
116
|
+
if (isOnboarded()) {
|
|
117
|
+
return res.json({ ok: false, error: "Already onboarded" });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const githubRepoInput = String(req.body?.repo || "").trim();
|
|
122
|
+
const githubToken = String(req.body?.token || "").trim();
|
|
123
|
+
if (!githubRepoInput || !githubToken) {
|
|
124
|
+
return res
|
|
125
|
+
.status(400)
|
|
126
|
+
.json({
|
|
127
|
+
ok: false,
|
|
128
|
+
error: "GitHub token and workspace repo are required",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = await onboardingService.verifyGithubSetup({
|
|
133
|
+
githubRepoInput,
|
|
134
|
+
githubToken,
|
|
135
|
+
resolveGithubRepoUrl,
|
|
136
|
+
});
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
return res
|
|
139
|
+
.status(result.status || 400)
|
|
140
|
+
.json({ ok: false, error: result.error });
|
|
141
|
+
}
|
|
142
|
+
return res.json({ ok: true });
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error("[onboard] GitHub verify error:", err);
|
|
145
|
+
return res
|
|
146
|
+
.status(500)
|
|
147
|
+
.json({ ok: false, error: sanitizeOnboardingError(err) });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
114
150
|
};
|
|
115
151
|
|
|
116
152
|
module.exports = { registerOnboardingRoutes };
|
|
@@ -3,13 +3,13 @@ const registerProxyRoutes = ({ app, proxy, SETUP_API_PREFIXES, requireAuth }) =>
|
|
|
3
3
|
req.url = "/";
|
|
4
4
|
proxy.web(req, res);
|
|
5
5
|
});
|
|
6
|
-
app.all("/openclaw/*", requireAuth, (req, res) => {
|
|
6
|
+
app.all("/openclaw/*path", requireAuth, (req, res) => {
|
|
7
7
|
req.url = req.url.replace(/^\/openclaw/, "");
|
|
8
8
|
proxy.web(req, res);
|
|
9
9
|
});
|
|
10
|
-
app.all("/assets/*", requireAuth, (req, res) => proxy.web(req, res));
|
|
10
|
+
app.all("/assets/*path", requireAuth, (req, res) => proxy.web(req, res));
|
|
11
11
|
|
|
12
|
-
app.all("/webhook/*", (req, res) => {
|
|
12
|
+
app.all("/webhook/*path", (req, res) => {
|
|
13
13
|
if (!req.headers.authorization && req.query.token) {
|
|
14
14
|
req.headers.authorization = `Bearer ${req.query.token}`;
|
|
15
15
|
delete req.query.token;
|
|
@@ -20,7 +20,7 @@ const registerProxyRoutes = ({ app, proxy, SETUP_API_PREFIXES, requireAuth }) =>
|
|
|
20
20
|
proxy.web(req, res);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
app.all("/api/*", (req, res) => {
|
|
23
|
+
app.all("/api/*path", (req, res) => {
|
|
24
24
|
if (SETUP_API_PREFIXES.some((p) => req.path.startsWith(p))) return;
|
|
25
25
|
proxy.web(req, res);
|
|
26
26
|
});
|