@chrysb/alphaclaw 0.1.0
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 +338 -0
- package/lib/public/icons/chevron-down.svg +9 -0
- package/lib/public/js/app.js +325 -0
- package/lib/public/js/components/badge.js +16 -0
- package/lib/public/js/components/channels.js +36 -0
- package/lib/public/js/components/credentials-modal.js +336 -0
- package/lib/public/js/components/device-pairings.js +72 -0
- package/lib/public/js/components/envars.js +354 -0
- package/lib/public/js/components/gateway.js +163 -0
- package/lib/public/js/components/google.js +223 -0
- package/lib/public/js/components/icons.js +23 -0
- package/lib/public/js/components/models.js +461 -0
- package/lib/public/js/components/pairings.js +74 -0
- package/lib/public/js/components/scope-picker.js +106 -0
- package/lib/public/js/components/toast.js +31 -0
- package/lib/public/js/components/welcome.js +541 -0
- package/lib/public/js/hooks/usePolling.js +29 -0
- package/lib/public/js/lib/api.js +196 -0
- package/lib/public/js/lib/model-config.js +88 -0
- package/lib/public/login.html +90 -0
- package/lib/public/setup.html +33 -0
- package/lib/scripts/systemctl +56 -0
- package/lib/server/auth-profiles.js +101 -0
- package/lib/server/commands.js +84 -0
- package/lib/server/constants.js +282 -0
- package/lib/server/env.js +78 -0
- package/lib/server/gateway.js +262 -0
- package/lib/server/helpers.js +192 -0
- package/lib/server/login-throttle.js +86 -0
- package/lib/server/onboarding/cron.js +51 -0
- package/lib/server/onboarding/github.js +49 -0
- package/lib/server/onboarding/index.js +127 -0
- package/lib/server/onboarding/openclaw.js +171 -0
- package/lib/server/onboarding/validation.js +107 -0
- package/lib/server/onboarding/workspace.js +52 -0
- package/lib/server/openclaw-version.js +179 -0
- package/lib/server/routes/auth.js +80 -0
- package/lib/server/routes/codex.js +204 -0
- package/lib/server/routes/google.js +390 -0
- package/lib/server/routes/models.js +68 -0
- package/lib/server/routes/onboarding.js +116 -0
- package/lib/server/routes/pages.js +21 -0
- package/lib/server/routes/pairings.js +134 -0
- package/lib/server/routes/proxy.js +29 -0
- package/lib/server/routes/system.js +213 -0
- package/lib/server.js +161 -0
- package/lib/setup/core-prompts/AGENTS.md +22 -0
- package/lib/setup/core-prompts/TOOLS.md +18 -0
- package/lib/setup/env.template +19 -0
- package/lib/setup/gitignore +12 -0
- package/lib/setup/hourly-git-sync.sh +86 -0
- package/lib/setup/skills/control-ui/SKILL.md +70 -0
- package/package.json +34 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
const {
|
|
4
|
+
CODEX_JWT_CLAIM_PATH,
|
|
5
|
+
kOnboardingModelProviders,
|
|
6
|
+
GOG_CREDENTIALS_PATH,
|
|
7
|
+
} = require("./constants");
|
|
8
|
+
|
|
9
|
+
const normalizeOpenclawVersion = (rawVersion) => {
|
|
10
|
+
if (!rawVersion) return null;
|
|
11
|
+
return String(rawVersion).trim().replace(/^openclaw\s*/i, "") || null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const compareVersionParts = (a, b) => {
|
|
15
|
+
const aParts = String(a || "")
|
|
16
|
+
.split(".")
|
|
17
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
18
|
+
const bParts = String(b || "")
|
|
19
|
+
.split(".")
|
|
20
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
21
|
+
const maxParts = Math.max(aParts.length, bParts.length);
|
|
22
|
+
for (let i = 0; i < maxParts; i += 1) {
|
|
23
|
+
const aPart = aParts[i] ?? 0;
|
|
24
|
+
const bPart = bParts[i] ?? 0;
|
|
25
|
+
if (aPart > bPart) return 1;
|
|
26
|
+
if (aPart < bPart) return -1;
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const parseJsonFromNoisyOutput = (raw) => {
|
|
32
|
+
const text = String(raw || "");
|
|
33
|
+
const firstBrace = text.indexOf("{");
|
|
34
|
+
const lastBrace = text.lastIndexOf("}");
|
|
35
|
+
if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(text.slice(firstBrace, lastBrace + 1));
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const parseJwtPayload = (token) => {
|
|
46
|
+
try {
|
|
47
|
+
const parts = String(token || "").split(".");
|
|
48
|
+
if (parts.length !== 3) return null;
|
|
49
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getCodexAccountId = (accessToken) => {
|
|
56
|
+
const payload = parseJwtPayload(accessToken);
|
|
57
|
+
const auth = payload?.[CODEX_JWT_CLAIM_PATH];
|
|
58
|
+
const accountId = auth?.chatgpt_account_id;
|
|
59
|
+
return typeof accountId === "string" && accountId ? accountId : null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const normalizeIp = (ip) => String(ip || "").replace(/^::ffff:/, "");
|
|
63
|
+
|
|
64
|
+
const isTruthyEnvFlag = (value) =>
|
|
65
|
+
["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
|
|
66
|
+
|
|
67
|
+
const getClientKey = (req) =>
|
|
68
|
+
normalizeIp(
|
|
69
|
+
req.ip || req.headers["x-forwarded-for"] || req.socket?.remoteAddress || "",
|
|
70
|
+
) || "unknown";
|
|
71
|
+
|
|
72
|
+
const resolveGithubRepoUrl = (repoInput) => {
|
|
73
|
+
const cleaned = String(repoInput || "")
|
|
74
|
+
.trim()
|
|
75
|
+
.replace(/^git@github\.com:/, "")
|
|
76
|
+
.replace(/^https:\/\/github\.com\//, "")
|
|
77
|
+
.replace(/\.git$/, "");
|
|
78
|
+
if (!cleaned) return "";
|
|
79
|
+
if (!cleaned.includes("/")) {
|
|
80
|
+
throw new Error('GITHUB_WORKSPACE_REPO must be in "owner/repo" format.');
|
|
81
|
+
}
|
|
82
|
+
return cleaned;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const createPkcePair = () => {
|
|
86
|
+
const verifier = crypto.randomBytes(48).toString("base64url");
|
|
87
|
+
const challenge = crypto
|
|
88
|
+
.createHash("sha256")
|
|
89
|
+
.update(verifier)
|
|
90
|
+
.digest("base64url");
|
|
91
|
+
return { verifier, challenge };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const resolveModelProvider = (modelKey) =>
|
|
95
|
+
String(modelKey || "").split("/")[0] || "";
|
|
96
|
+
|
|
97
|
+
const parseCodexAuthorizationInput = (input) => {
|
|
98
|
+
const value = String(input || "").trim();
|
|
99
|
+
if (!value) return {};
|
|
100
|
+
try {
|
|
101
|
+
const url = new URL(value);
|
|
102
|
+
return {
|
|
103
|
+
code: url.searchParams.get("code") || "",
|
|
104
|
+
state: url.searchParams.get("state") || "",
|
|
105
|
+
};
|
|
106
|
+
} catch {}
|
|
107
|
+
if (value.includes("#")) {
|
|
108
|
+
const [code, state] = value.split("#", 2);
|
|
109
|
+
return { code: code || "", state: state || "" };
|
|
110
|
+
}
|
|
111
|
+
if (value.includes("code=")) {
|
|
112
|
+
const params = new URLSearchParams(value);
|
|
113
|
+
return {
|
|
114
|
+
code: params.get("code") || "",
|
|
115
|
+
state: params.get("state") || "",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { code: value, state: "" };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const normalizeOnboardingModels = (models) => {
|
|
122
|
+
const deduped = new Map();
|
|
123
|
+
for (const model of models || []) {
|
|
124
|
+
if (!model?.key || typeof model.key !== "string") continue;
|
|
125
|
+
const provider = resolveModelProvider(model.key);
|
|
126
|
+
if (!kOnboardingModelProviders.has(provider)) continue;
|
|
127
|
+
if (!deduped.has(model.key)) {
|
|
128
|
+
deduped.set(model.key, {
|
|
129
|
+
key: model.key,
|
|
130
|
+
provider,
|
|
131
|
+
label: model.name || model.key,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return Array.from(deduped.values()).sort((a, b) =>
|
|
136
|
+
a.key.localeCompare(b.key),
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const getBaseUrl = (req) => {
|
|
141
|
+
const proto = req.headers["x-forwarded-proto"] || req.protocol || "https";
|
|
142
|
+
const host = req.headers["x-forwarded-host"] || req.headers.host;
|
|
143
|
+
return `${proto}://${host}`;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const getApiEnableUrl = (svc, projectId) => {
|
|
147
|
+
const apiMap = {
|
|
148
|
+
gmail: "gmail.googleapis.com",
|
|
149
|
+
calendar: "calendar-json.googleapis.com",
|
|
150
|
+
tasks: "tasks.googleapis.com",
|
|
151
|
+
docs: "docs.googleapis.com",
|
|
152
|
+
meet: "meet.googleapis.com",
|
|
153
|
+
drive: "drive.googleapis.com",
|
|
154
|
+
contacts: "people.googleapis.com",
|
|
155
|
+
sheets: "sheets.googleapis.com",
|
|
156
|
+
};
|
|
157
|
+
const api = apiMap[svc] || "";
|
|
158
|
+
const project = projectId ? `?project=${projectId}` : "";
|
|
159
|
+
return `https://console.developers.google.com/apis/api/${api}/overview${project}`;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const readGoogleCredentials = () => {
|
|
163
|
+
try {
|
|
164
|
+
const c = JSON.parse(fs.readFileSync(GOG_CREDENTIALS_PATH, "utf8"));
|
|
165
|
+
return {
|
|
166
|
+
clientId: c.web?.client_id || c.installed?.client_id || c.client_id || null,
|
|
167
|
+
clientSecret:
|
|
168
|
+
c.web?.client_secret || c.installed?.client_secret || c.client_secret || null,
|
|
169
|
+
};
|
|
170
|
+
} catch {
|
|
171
|
+
return { clientId: null, clientSecret: null };
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
normalizeOpenclawVersion,
|
|
177
|
+
compareVersionParts,
|
|
178
|
+
parseJsonFromNoisyOutput,
|
|
179
|
+
parseJwtPayload,
|
|
180
|
+
getCodexAccountId,
|
|
181
|
+
normalizeIp,
|
|
182
|
+
isTruthyEnvFlag,
|
|
183
|
+
getClientKey,
|
|
184
|
+
resolveGithubRepoUrl,
|
|
185
|
+
createPkcePair,
|
|
186
|
+
resolveModelProvider,
|
|
187
|
+
parseCodexAuthorizationInput,
|
|
188
|
+
normalizeOnboardingModels,
|
|
189
|
+
getBaseUrl,
|
|
190
|
+
getApiEnableUrl,
|
|
191
|
+
readGoogleCredentials,
|
|
192
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { kLoginWindowMs, kLoginMaxAttempts, kLoginBaseLockMs, kLoginMaxLockMs, kLoginStateTtlMs } = require("./constants");
|
|
2
|
+
|
|
3
|
+
const createLoginThrottle = () => {
|
|
4
|
+
const kLoginAttemptStates = new Map();
|
|
5
|
+
|
|
6
|
+
const getOrCreateLoginAttemptState = (clientKey, now) => {
|
|
7
|
+
const existing = kLoginAttemptStates.get(clientKey);
|
|
8
|
+
if (existing) {
|
|
9
|
+
existing.lastSeenAt = now;
|
|
10
|
+
return existing;
|
|
11
|
+
}
|
|
12
|
+
const next = {
|
|
13
|
+
attempts: 0,
|
|
14
|
+
windowStart: now,
|
|
15
|
+
lockUntil: 0,
|
|
16
|
+
failStreak: 0,
|
|
17
|
+
lastSeenAt: now,
|
|
18
|
+
};
|
|
19
|
+
kLoginAttemptStates.set(clientKey, next);
|
|
20
|
+
return next;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const evaluateLoginThrottle = (state, now) => {
|
|
24
|
+
if (!state) return { blocked: false, retryAfterSec: 0 };
|
|
25
|
+
if (state.lockUntil > now) {
|
|
26
|
+
return {
|
|
27
|
+
blocked: true,
|
|
28
|
+
retryAfterSec: Math.max(1, Math.ceil((state.lockUntil - now) / 1000)),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (now - state.windowStart >= kLoginWindowMs) {
|
|
32
|
+
state.attempts = 0;
|
|
33
|
+
state.windowStart = now;
|
|
34
|
+
}
|
|
35
|
+
return { blocked: false, retryAfterSec: 0 };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const recordLoginFailure = (state, now) => {
|
|
39
|
+
if (!state) return { lockMs: 0, locked: false };
|
|
40
|
+
if (now - state.windowStart >= kLoginWindowMs) {
|
|
41
|
+
state.attempts = 0;
|
|
42
|
+
state.windowStart = now;
|
|
43
|
+
}
|
|
44
|
+
state.attempts += 1;
|
|
45
|
+
state.lastSeenAt = now;
|
|
46
|
+
if (state.attempts < kLoginMaxAttempts) {
|
|
47
|
+
return { lockMs: 0, locked: false };
|
|
48
|
+
}
|
|
49
|
+
state.failStreak += 1;
|
|
50
|
+
state.attempts = 0;
|
|
51
|
+
state.windowStart = now;
|
|
52
|
+
const lockMultiplier = Math.max(1, 2 ** (state.failStreak - 1));
|
|
53
|
+
const lockMs = Math.min(kLoginBaseLockMs * lockMultiplier, kLoginMaxLockMs);
|
|
54
|
+
state.lockUntil = now + lockMs;
|
|
55
|
+
return { lockMs, locked: true };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const recordLoginSuccess = (clientKey) => {
|
|
59
|
+
if (!clientKey) return;
|
|
60
|
+
kLoginAttemptStates.delete(clientKey);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const cleanupLoginAttemptStates = () => {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
for (const [key, state] of kLoginAttemptStates.entries()) {
|
|
66
|
+
if (!state) {
|
|
67
|
+
kLoginAttemptStates.delete(key);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (state.lockUntil > now) continue;
|
|
71
|
+
if (now - state.lastSeenAt > kLoginStateTtlMs) {
|
|
72
|
+
kLoginAttemptStates.delete(key);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
getOrCreateLoginAttemptState,
|
|
79
|
+
evaluateLoginThrottle,
|
|
80
|
+
recordLoginFailure,
|
|
81
|
+
recordLoginSuccess,
|
|
82
|
+
cleanupLoginAttemptStates,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
module.exports = { createLoginThrottle };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { kSetupDir } = require("../constants");
|
|
3
|
+
|
|
4
|
+
const kHourlyGitSyncTemplatePath = path.join(kSetupDir, "hourly-git-sync.sh");
|
|
5
|
+
const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
|
|
6
|
+
const kSystemCronConfigDir = "cron";
|
|
7
|
+
const kSystemCronConfigFile = "system-sync.json";
|
|
8
|
+
const kDefaultSystemCronSchedule = "0 * * * *";
|
|
9
|
+
|
|
10
|
+
const buildSystemCronFile = ({ schedule, scriptPath }) =>
|
|
11
|
+
[
|
|
12
|
+
"SHELL=/bin/bash",
|
|
13
|
+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
14
|
+
`${schedule} root bash "${scriptPath}" >> /var/log/openclaw-hourly-sync.log 2>&1`,
|
|
15
|
+
"",
|
|
16
|
+
].join("\n");
|
|
17
|
+
|
|
18
|
+
const installHourlyGitSyncScript = ({ fs, openclawDir }) => {
|
|
19
|
+
try {
|
|
20
|
+
const scriptPath = `${openclawDir}/hourly-git-sync.sh`;
|
|
21
|
+
const hourlyGitSyncScript = fs.readFileSync(kHourlyGitSyncTemplatePath, "utf8");
|
|
22
|
+
fs.writeFileSync(scriptPath, hourlyGitSyncScript, { mode: 0o755 });
|
|
23
|
+
console.log("[onboard] Installed deterministic hourly git sync script");
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error("[onboard] Hourly git sync script install error:", e.message);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const installHourlyGitSyncCron = async ({ fs, openclawDir }) => {
|
|
30
|
+
try {
|
|
31
|
+
const scriptPath = `${openclawDir}/hourly-git-sync.sh`;
|
|
32
|
+
const configDir = `${openclawDir}/${kSystemCronConfigDir}`;
|
|
33
|
+
const configPath = `${configDir}/${kSystemCronConfigFile}`;
|
|
34
|
+
const config = { enabled: true, schedule: kDefaultSystemCronSchedule };
|
|
35
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
36
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
37
|
+
|
|
38
|
+
const cronContent = buildSystemCronFile({
|
|
39
|
+
schedule: config.schedule,
|
|
40
|
+
scriptPath,
|
|
41
|
+
});
|
|
42
|
+
fs.writeFileSync(kSystemCronPath, cronContent, { mode: 0o644 });
|
|
43
|
+
console.log(`[onboard] Installed system cron job at ${kSystemCronPath} (${configPath})`);
|
|
44
|
+
return true;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error("[onboard] System cron install error:", e.message);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = { installHourlyGitSyncScript, installHourlyGitSyncCron };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const ensureGithubRepoAccessible = async ({ repoUrl, repoName, remoteUrl, githubToken }) => {
|
|
2
|
+
void remoteUrl;
|
|
3
|
+
const ghHeaders = {
|
|
4
|
+
Authorization: `token ${githubToken}`,
|
|
5
|
+
"User-Agent": "openclaw-railway",
|
|
6
|
+
Accept: "application/vnd.github+json",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const checkRes = await fetch(`https://api.github.com/repos/${repoUrl}`, {
|
|
11
|
+
headers: ghHeaders,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
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
|
+
return { ok: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (checkRes.ok) return { ok: true };
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
status: 400,
|
|
42
|
+
error: `Cannot access repo "${repoUrl}" — check your token has the "repo" scope`,
|
|
43
|
+
};
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return { ok: false, status: 400, error: `GitHub error: ${e.message}` };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
module.exports = { ensureGithubRepoAccessible };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { kSetupDir, kRootDir } = require("../constants");
|
|
3
|
+
const { validateOnboardingInput } = require("./validation");
|
|
4
|
+
const { ensureGithubRepoAccessible } = require("./github");
|
|
5
|
+
const { buildOnboardArgs, writeSanitizedOpenclawConfig } = require("./openclaw");
|
|
6
|
+
const { installControlUiSkill, syncBootstrapPromptFiles } = require("./workspace");
|
|
7
|
+
const { installHourlyGitSyncScript, installHourlyGitSyncCron } = require("./cron");
|
|
8
|
+
const { isTruthyEnvFlag } = require("../helpers");
|
|
9
|
+
|
|
10
|
+
const createOnboardingService = ({
|
|
11
|
+
fs,
|
|
12
|
+
constants,
|
|
13
|
+
shellCmd,
|
|
14
|
+
gatewayEnv,
|
|
15
|
+
writeEnvFile,
|
|
16
|
+
reloadEnv,
|
|
17
|
+
resolveGithubRepoUrl,
|
|
18
|
+
resolveModelProvider,
|
|
19
|
+
hasCodexOauthProfile,
|
|
20
|
+
ensureGatewayProxyConfig,
|
|
21
|
+
getBaseUrl,
|
|
22
|
+
startGateway,
|
|
23
|
+
}) => {
|
|
24
|
+
const { OPENCLAW_DIR, WORKSPACE_DIR } = constants;
|
|
25
|
+
|
|
26
|
+
const completeOnboarding = async ({ req, vars, modelKey }) => {
|
|
27
|
+
const validation = validateOnboardingInput({
|
|
28
|
+
vars,
|
|
29
|
+
modelKey,
|
|
30
|
+
resolveModelProvider,
|
|
31
|
+
hasCodexOauthProfile,
|
|
32
|
+
});
|
|
33
|
+
if (!validation.ok) {
|
|
34
|
+
return { status: validation.status, body: { ok: false, error: validation.error } };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { varMap, githubToken, githubRepoInput, selectedProvider, hasCodexOauth } =
|
|
38
|
+
validation.data;
|
|
39
|
+
|
|
40
|
+
const repoUrl = resolveGithubRepoUrl(githubRepoInput);
|
|
41
|
+
const varsToSave = [...vars.filter((v) => v.value && v.key !== "GITHUB_WORKSPACE_REPO")];
|
|
42
|
+
varsToSave.push({ key: "GITHUB_WORKSPACE_REPO", value: repoUrl });
|
|
43
|
+
writeEnvFile(varsToSave);
|
|
44
|
+
reloadEnv();
|
|
45
|
+
|
|
46
|
+
const remoteUrl = `https://${githubToken}@github.com/${repoUrl}.git`;
|
|
47
|
+
const [, repoName] = repoUrl.split("/");
|
|
48
|
+
const repoCheck = await ensureGithubRepoAccessible({
|
|
49
|
+
repoUrl,
|
|
50
|
+
repoName,
|
|
51
|
+
remoteUrl,
|
|
52
|
+
githubToken,
|
|
53
|
+
});
|
|
54
|
+
if (!repoCheck.ok) {
|
|
55
|
+
return { status: repoCheck.status, body: { ok: false, error: repoCheck.error } };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
|
59
|
+
fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
|
|
60
|
+
syncBootstrapPromptFiles({ fs, workspaceDir: WORKSPACE_DIR, baseUrl: getBaseUrl(req) });
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(`${OPENCLAW_DIR}/.git`)) {
|
|
63
|
+
await shellCmd(
|
|
64
|
+
`cd ${OPENCLAW_DIR} && git init -b main && git remote add origin "${remoteUrl}" && git config user.email "agent@openclaw.ai" && git config user.name "OpenClaw Agent"`,
|
|
65
|
+
);
|
|
66
|
+
console.log("[onboard] Git initialized");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(`${OPENCLAW_DIR}/.gitignore`)) {
|
|
70
|
+
fs.copyFileSync(path.join(kSetupDir, "gitignore"), `${OPENCLAW_DIR}/.gitignore`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const onboardArgs = buildOnboardArgs({
|
|
74
|
+
varMap,
|
|
75
|
+
selectedProvider,
|
|
76
|
+
hasCodexOauth,
|
|
77
|
+
workspaceDir: WORKSPACE_DIR,
|
|
78
|
+
});
|
|
79
|
+
console.log(
|
|
80
|
+
`[onboard] Running: openclaw onboard ${onboardArgs.join(" ").replace(/sk-[^\s]+/g, "***")}`,
|
|
81
|
+
);
|
|
82
|
+
await shellCmd(`openclaw onboard ${onboardArgs.map((a) => `"${a}"`).join(" ")}`, {
|
|
83
|
+
env: {
|
|
84
|
+
...process.env,
|
|
85
|
+
OPENCLAW_HOME: kRootDir,
|
|
86
|
+
OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
|
|
87
|
+
},
|
|
88
|
+
timeout: 120000,
|
|
89
|
+
});
|
|
90
|
+
console.log("[onboard] Onboard complete");
|
|
91
|
+
|
|
92
|
+
await shellCmd(`openclaw models set "${modelKey}"`, {
|
|
93
|
+
env: gatewayEnv(),
|
|
94
|
+
timeout: 30000,
|
|
95
|
+
}).catch((e) => {
|
|
96
|
+
console.error("[onboard] Failed to set model:", e.message);
|
|
97
|
+
throw new Error(`Onboarding completed but failed to set model "${modelKey}"`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
fs.rmSync(`${WORKSPACE_DIR}/.git`, { recursive: true, force: true });
|
|
102
|
+
} catch {}
|
|
103
|
+
|
|
104
|
+
writeSanitizedOpenclawConfig({ fs, openclawDir: OPENCLAW_DIR, varMap });
|
|
105
|
+
ensureGatewayProxyConfig(getBaseUrl(req));
|
|
106
|
+
|
|
107
|
+
installControlUiSkill({ fs, openclawDir: OPENCLAW_DIR, baseUrl: getBaseUrl(req) });
|
|
108
|
+
|
|
109
|
+
const shouldForcePush = isTruthyEnvFlag(process.env.OPENCLAW_DEBUG_FORCE_PUSH);
|
|
110
|
+
const pushArgs = shouldForcePush ? "-u --force" : "-u";
|
|
111
|
+
await shellCmd(
|
|
112
|
+
`cd ${OPENCLAW_DIR} && git add -A && git commit -m "initial setup" && git push ${pushArgs} origin main`,
|
|
113
|
+
{ timeout: 30000 },
|
|
114
|
+
).catch((e) => console.error("[onboard] Git push error:", e.message));
|
|
115
|
+
console.log("[onboard] Initial state committed and pushed");
|
|
116
|
+
|
|
117
|
+
installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
|
|
118
|
+
await installHourlyGitSyncCron({ fs, openclawDir: OPENCLAW_DIR });
|
|
119
|
+
|
|
120
|
+
startGateway();
|
|
121
|
+
return { status: 200, body: { ok: true } };
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return { completeOnboarding };
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
module.exports = { createOnboardingService };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const buildOnboardArgs = ({ varMap, selectedProvider, hasCodexOauth, workspaceDir }) => {
|
|
2
|
+
const onboardArgs = [
|
|
3
|
+
"--non-interactive",
|
|
4
|
+
"--accept-risk",
|
|
5
|
+
"--flow",
|
|
6
|
+
"quickstart",
|
|
7
|
+
"--gateway-bind",
|
|
8
|
+
"loopback",
|
|
9
|
+
"--gateway-port",
|
|
10
|
+
"18789",
|
|
11
|
+
"--gateway-auth",
|
|
12
|
+
"token",
|
|
13
|
+
"--gateway-token",
|
|
14
|
+
varMap.OPENCLAW_GATEWAY_TOKEN || process.env.OPENCLAW_GATEWAY_TOKEN || "",
|
|
15
|
+
"--no-install-daemon",
|
|
16
|
+
"--skip-health",
|
|
17
|
+
"--workspace",
|
|
18
|
+
workspaceDir,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
selectedProvider === "openai-codex" &&
|
|
23
|
+
(varMap.OPENAI_API_KEY || process.env.OPENAI_API_KEY)
|
|
24
|
+
) {
|
|
25
|
+
onboardArgs.push(
|
|
26
|
+
"--auth-choice",
|
|
27
|
+
"openai-api-key",
|
|
28
|
+
"--openai-api-key",
|
|
29
|
+
varMap.OPENAI_API_KEY || process.env.OPENAI_API_KEY,
|
|
30
|
+
);
|
|
31
|
+
} else if (selectedProvider === "openai-codex" && hasCodexOauth) {
|
|
32
|
+
onboardArgs.push("--auth-choice", "skip");
|
|
33
|
+
} else if (
|
|
34
|
+
(selectedProvider === "anthropic" || !selectedProvider) &&
|
|
35
|
+
(varMap.ANTHROPIC_TOKEN || process.env.ANTHROPIC_TOKEN)
|
|
36
|
+
) {
|
|
37
|
+
onboardArgs.push(
|
|
38
|
+
"--auth-choice",
|
|
39
|
+
"token",
|
|
40
|
+
"--token-provider",
|
|
41
|
+
"anthropic",
|
|
42
|
+
"--token",
|
|
43
|
+
varMap.ANTHROPIC_TOKEN || process.env.ANTHROPIC_TOKEN,
|
|
44
|
+
);
|
|
45
|
+
} else if (
|
|
46
|
+
(selectedProvider === "anthropic" || !selectedProvider) &&
|
|
47
|
+
(varMap.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY)
|
|
48
|
+
) {
|
|
49
|
+
onboardArgs.push(
|
|
50
|
+
"--auth-choice",
|
|
51
|
+
"apiKey",
|
|
52
|
+
"--anthropic-api-key",
|
|
53
|
+
varMap.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY,
|
|
54
|
+
);
|
|
55
|
+
} else if (
|
|
56
|
+
(selectedProvider === "openai" || !selectedProvider) &&
|
|
57
|
+
(varMap.OPENAI_API_KEY || process.env.OPENAI_API_KEY)
|
|
58
|
+
) {
|
|
59
|
+
onboardArgs.push(
|
|
60
|
+
"--auth-choice",
|
|
61
|
+
"openai-api-key",
|
|
62
|
+
"--openai-api-key",
|
|
63
|
+
varMap.OPENAI_API_KEY || process.env.OPENAI_API_KEY,
|
|
64
|
+
);
|
|
65
|
+
} else if (
|
|
66
|
+
(selectedProvider === "google" || !selectedProvider) &&
|
|
67
|
+
(varMap.GEMINI_API_KEY || process.env.GEMINI_API_KEY)
|
|
68
|
+
) {
|
|
69
|
+
onboardArgs.push(
|
|
70
|
+
"--auth-choice",
|
|
71
|
+
"gemini-api-key",
|
|
72
|
+
"--gemini-api-key",
|
|
73
|
+
varMap.GEMINI_API_KEY || process.env.GEMINI_API_KEY,
|
|
74
|
+
);
|
|
75
|
+
} else if (varMap.ANTHROPIC_TOKEN || process.env.ANTHROPIC_TOKEN) {
|
|
76
|
+
onboardArgs.push(
|
|
77
|
+
"--auth-choice",
|
|
78
|
+
"token",
|
|
79
|
+
"--token-provider",
|
|
80
|
+
"anthropic",
|
|
81
|
+
"--token",
|
|
82
|
+
varMap.ANTHROPIC_TOKEN || process.env.ANTHROPIC_TOKEN,
|
|
83
|
+
);
|
|
84
|
+
} else if (varMap.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY) {
|
|
85
|
+
onboardArgs.push(
|
|
86
|
+
"--auth-choice",
|
|
87
|
+
"apiKey",
|
|
88
|
+
"--anthropic-api-key",
|
|
89
|
+
varMap.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY,
|
|
90
|
+
);
|
|
91
|
+
} else if (varMap.OPENAI_API_KEY || process.env.OPENAI_API_KEY) {
|
|
92
|
+
onboardArgs.push(
|
|
93
|
+
"--auth-choice",
|
|
94
|
+
"openai-api-key",
|
|
95
|
+
"--openai-api-key",
|
|
96
|
+
varMap.OPENAI_API_KEY || process.env.OPENAI_API_KEY,
|
|
97
|
+
);
|
|
98
|
+
} else if (varMap.GEMINI_API_KEY || process.env.GEMINI_API_KEY) {
|
|
99
|
+
onboardArgs.push(
|
|
100
|
+
"--auth-choice",
|
|
101
|
+
"gemini-api-key",
|
|
102
|
+
"--gemini-api-key",
|
|
103
|
+
varMap.GEMINI_API_KEY || process.env.GEMINI_API_KEY,
|
|
104
|
+
);
|
|
105
|
+
} else if (hasCodexOauth) {
|
|
106
|
+
onboardArgs.push("--auth-choice", "skip");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return onboardArgs;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
|
|
113
|
+
const configPath = `${openclawDir}/openclaw.json`;
|
|
114
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
115
|
+
if (!cfg.channels) cfg.channels = {};
|
|
116
|
+
if (!cfg.plugins) cfg.plugins = {};
|
|
117
|
+
if (!cfg.plugins.entries) cfg.plugins.entries = {};
|
|
118
|
+
if (!cfg.commands) cfg.commands = {};
|
|
119
|
+
if (!cfg.hooks) cfg.hooks = {};
|
|
120
|
+
if (!cfg.hooks.internal) cfg.hooks.internal = {};
|
|
121
|
+
if (!cfg.hooks.internal.entries) cfg.hooks.internal.entries = {};
|
|
122
|
+
cfg.commands.restart = true;
|
|
123
|
+
cfg.hooks.internal.enabled = true;
|
|
124
|
+
cfg.hooks.internal.entries["bootstrap-extra-files"] = {
|
|
125
|
+
...(cfg.hooks.internal.entries["bootstrap-extra-files"] || {}),
|
|
126
|
+
enabled: true,
|
|
127
|
+
paths: ["hooks/bootstrap/AGENTS.md", "hooks/bootstrap/TOOLS.md"],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (varMap.TELEGRAM_BOT_TOKEN) {
|
|
131
|
+
cfg.channels.telegram = {
|
|
132
|
+
enabled: true,
|
|
133
|
+
botToken: varMap.TELEGRAM_BOT_TOKEN,
|
|
134
|
+
dmPolicy: "pairing",
|
|
135
|
+
groupPolicy: "allowlist",
|
|
136
|
+
};
|
|
137
|
+
cfg.plugins.entries.telegram = { enabled: true };
|
|
138
|
+
console.log("[onboard] Telegram configured");
|
|
139
|
+
}
|
|
140
|
+
if (varMap.DISCORD_BOT_TOKEN) {
|
|
141
|
+
cfg.channels.discord = {
|
|
142
|
+
enabled: true,
|
|
143
|
+
token: varMap.DISCORD_BOT_TOKEN,
|
|
144
|
+
dmPolicy: "pairing",
|
|
145
|
+
groupPolicy: "allowlist",
|
|
146
|
+
};
|
|
147
|
+
cfg.plugins.entries.discord = { enabled: true };
|
|
148
|
+
console.log("[onboard] Discord configured");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
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
|
+
];
|
|
162
|
+
for (const [secret, envRef] of replacements) {
|
|
163
|
+
if (secret && secret.length > 8) {
|
|
164
|
+
content = content.split(secret).join(envRef);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
fs.writeFileSync(configPath, content);
|
|
168
|
+
console.log("[onboard] Config sanitized");
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
module.exports = { buildOnboardArgs, writeSanitizedOpenclawConfig };
|