@chrysb/alphaclaw 0.4.6-beta.7 → 0.4.6-beta.9
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 +2 -32
- package/lib/public/css/theme.css +19 -0
- package/lib/public/js/app.js +1 -1
- package/lib/public/js/components/doctor/helpers.js +71 -5
- package/lib/public/js/components/doctor/index.js +89 -28
- package/lib/public/js/components/envars.js +0 -1
- package/lib/public/js/components/onboarding/welcome-config.js +39 -17
- package/lib/public/js/components/onboarding/welcome-form-step.js +142 -47
- package/lib/public/js/components/onboarding/welcome-import-step.js +306 -0
- package/lib/public/js/components/onboarding/welcome-placeholder-review-step.js +99 -0
- package/lib/public/js/components/onboarding/welcome-secret-review-step.js +191 -0
- package/lib/public/js/components/segmented-control.js +7 -1
- package/lib/public/js/components/welcome/index.js +112 -0
- package/lib/public/js/components/welcome/use-welcome.js +561 -0
- package/lib/public/js/lib/api.js +221 -161
- package/lib/server/commands.js +1 -0
- package/lib/server/constants.js +0 -1
- package/lib/server/doctor/bootstrap-context.js +191 -0
- package/lib/server/doctor/prompt.js +20 -4
- package/lib/server/doctor/service.js +18 -4
- package/lib/server/gateway.js +15 -40
- package/lib/server/onboarding/github.js +120 -19
- package/lib/server/onboarding/import/import-applier.js +321 -0
- package/lib/server/onboarding/import/import-config.js +69 -0
- package/lib/server/onboarding/import/import-scanner.js +469 -0
- package/lib/server/onboarding/import/import-temp.js +63 -0
- package/lib/server/onboarding/import/secret-detector.js +289 -0
- package/lib/server/onboarding/index.js +256 -29
- package/lib/server/onboarding/workspace.js +38 -6
- package/lib/server/routes/onboarding.js +281 -12
- package/lib/server.js +12 -3
- package/package.json +1 -1
- package/lib/public/js/components/welcome.js +0 -318
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const kSecretKeyPatterns = [
|
|
4
|
+
/token$/i,
|
|
5
|
+
/^bot_?token$/i,
|
|
6
|
+
/api_?key$/i,
|
|
7
|
+
/secret$/i,
|
|
8
|
+
/password$/i,
|
|
9
|
+
/private_?key$/i,
|
|
10
|
+
/credential/i,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const kSafeKeyExclusions = [
|
|
14
|
+
/^auth_?dir$/i,
|
|
15
|
+
/^auth_?store$/i,
|
|
16
|
+
/^auto_?select/i,
|
|
17
|
+
/^public_?key$/i,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const kValuePrefixes = [
|
|
21
|
+
{ prefix: "sk-", label: "OpenAI/Anthropic/Stripe" },
|
|
22
|
+
{ prefix: "sk-ant-", label: "Anthropic" },
|
|
23
|
+
{ prefix: "sk-proj-", label: "OpenAI project" },
|
|
24
|
+
{ prefix: "ghp_", label: "GitHub classic PAT" },
|
|
25
|
+
{ prefix: "github_pat_", label: "GitHub fine-grained PAT" },
|
|
26
|
+
{ prefix: "ghs_", label: "GitHub App token" },
|
|
27
|
+
{ prefix: "gho_", label: "GitHub OAuth" },
|
|
28
|
+
{ prefix: "xoxb-", label: "Slack bot" },
|
|
29
|
+
{ prefix: "xoxp-", label: "Slack user" },
|
|
30
|
+
{ prefix: "xoxe-", label: "Slack enterprise" },
|
|
31
|
+
{ prefix: "xoxa-", label: "Slack app" },
|
|
32
|
+
{ prefix: "AIza", label: "Google API key" },
|
|
33
|
+
{ prefix: "ya29.", label: "Google OAuth" },
|
|
34
|
+
{ prefix: "AKIA", label: "AWS access key" },
|
|
35
|
+
{ prefix: "ntn_", label: "Notion" },
|
|
36
|
+
{ prefix: "nvapi-", label: "NVIDIA" },
|
|
37
|
+
{ prefix: "r8_", label: "Replicate" },
|
|
38
|
+
{ prefix: "hf_", label: "Hugging Face" },
|
|
39
|
+
{ prefix: "pk_live_", label: "Stripe publishable" },
|
|
40
|
+
{ prefix: "sk_live_", label: "Stripe secret" },
|
|
41
|
+
{ prefix: "pk_test_", label: "Stripe test pub" },
|
|
42
|
+
{ prefix: "sk_test_", label: "Stripe test secret" },
|
|
43
|
+
{ prefix: "whsec_", label: "Stripe webhook" },
|
|
44
|
+
{ prefix: "SG.", label: "SendGrid" },
|
|
45
|
+
{ prefix: "xai-", label: "xAI/Grok" },
|
|
46
|
+
{ prefix: "eyJ", label: "JWT" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Explicit config path -> env var name mapping
|
|
50
|
+
const kConfigPathToEnvVar = {
|
|
51
|
+
"channels.telegram.botToken": "TELEGRAM_BOT_TOKEN",
|
|
52
|
+
"channels.discord.token": "DISCORD_BOT_TOKEN",
|
|
53
|
+
"channels.slack.botToken": "SLACK_BOT_TOKEN",
|
|
54
|
+
"channels.slack.appToken": "SLACK_APP_TOKEN",
|
|
55
|
+
"channels.googlechat.serviceAccount": "GOOGLE_CHAT_SERVICE_ACCOUNT",
|
|
56
|
+
"channels.mattermost.botToken": "MATTERMOST_BOT_TOKEN",
|
|
57
|
+
"channels.mattermost.url": "MATTERMOST_URL",
|
|
58
|
+
"channels.twitch.accessToken": "OPENCLAW_TWITCH_ACCESS_TOKEN",
|
|
59
|
+
"models.providers.openai.apiKey": "OPENAI_API_KEY",
|
|
60
|
+
"models.providers.anthropic.apiKey": "ANTHROPIC_API_KEY",
|
|
61
|
+
"models.providers.google.apiKey": "GEMINI_API_KEY",
|
|
62
|
+
"models.providers.openrouter.apiKey": "OPENROUTER_API_KEY",
|
|
63
|
+
"models.providers.mistral.apiKey": "MISTRAL_API_KEY",
|
|
64
|
+
"models.providers.groq.apiKey": "GROQ_API_KEY",
|
|
65
|
+
"models.providers.cerebras.apiKey": "CEREBRAS_API_KEY",
|
|
66
|
+
"models.providers.voyage.apiKey": "VOYAGE_API_KEY",
|
|
67
|
+
"tools.web.search.apiKey": "BRAVE_API_KEY",
|
|
68
|
+
"audio.apiKey": "ELEVENLABS_API_KEY",
|
|
69
|
+
"talk.apiKey": "ELEVENLABS_API_KEY",
|
|
70
|
+
"gateway.auth.token": null, // Dropped — set at deploy time
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const isSensitiveKey = (key) => {
|
|
74
|
+
const str = String(key || "");
|
|
75
|
+
if (kSafeKeyExclusions.some((p) => p.test(str))) return false;
|
|
76
|
+
return kSecretKeyPatterns.some((p) => p.test(str));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const matchesValuePrefix = (value) => {
|
|
80
|
+
const str = String(value || "");
|
|
81
|
+
for (const { prefix, label } of kValuePrefixes) {
|
|
82
|
+
if (str.startsWith(prefix)) return { matched: true, label };
|
|
83
|
+
}
|
|
84
|
+
return { matched: false };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const isLikelyNonSecret = (value) => {
|
|
88
|
+
const str = String(value || "").trim();
|
|
89
|
+
if (str.length < 16) return true;
|
|
90
|
+
if (/^(true|false)$/i.test(str)) return true;
|
|
91
|
+
if (/^https?:\/\//.test(str) && !str.includes("token") && !str.includes("key")) return true;
|
|
92
|
+
if (/^[a-z0-9/-]+$/.test(str) && str.includes("/")) return true;
|
|
93
|
+
return false;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const maskValue = (value) => {
|
|
97
|
+
const str = String(value || "");
|
|
98
|
+
if (str.length <= 8) return "****";
|
|
99
|
+
return str.slice(0, 4) + "****" + str.slice(-4);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const configPathToEnvName = (dotPath) => {
|
|
103
|
+
if (kConfigPathToEnvVar[dotPath] !== undefined) {
|
|
104
|
+
return kConfigPathToEnvVar[dotPath];
|
|
105
|
+
}
|
|
106
|
+
const lastKey = dotPath.split(".").pop() || "";
|
|
107
|
+
return lastKey
|
|
108
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
109
|
+
.toUpperCase()
|
|
110
|
+
.replace(/[^A-Z0-9_]/g, "_");
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const walkConfig = (obj, parentPath, results) => {
|
|
114
|
+
if (!obj || typeof obj !== "object") return;
|
|
115
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
116
|
+
const dotPath = parentPath ? `${parentPath}.${key}` : key;
|
|
117
|
+
|
|
118
|
+
if (typeof value === "string" && value.trim()) {
|
|
119
|
+
const explicitEnvVar = kConfigPathToEnvVar[dotPath];
|
|
120
|
+
if (explicitEnvVar !== undefined) {
|
|
121
|
+
if (explicitEnvVar === null) continue;
|
|
122
|
+
if (!isAlreadyEnvRef(value)) {
|
|
123
|
+
results.push({
|
|
124
|
+
configPath: dotPath,
|
|
125
|
+
key,
|
|
126
|
+
value,
|
|
127
|
+
maskedValue: maskValue(value),
|
|
128
|
+
suggestedEnvVar: explicitEnvVar,
|
|
129
|
+
confidence: "high",
|
|
130
|
+
source: "config-path",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const prefixMatch = matchesValuePrefix(value);
|
|
137
|
+
if (prefixMatch.matched) {
|
|
138
|
+
if (!isAlreadyEnvRef(value)) {
|
|
139
|
+
results.push({
|
|
140
|
+
configPath: dotPath,
|
|
141
|
+
key,
|
|
142
|
+
value,
|
|
143
|
+
maskedValue: maskValue(value),
|
|
144
|
+
suggestedEnvVar: configPathToEnvName(dotPath),
|
|
145
|
+
confidence: "high",
|
|
146
|
+
source: "value-prefix",
|
|
147
|
+
prefixLabel: prefixMatch.label,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (isSensitiveKey(key) && !isLikelyNonSecret(value)) {
|
|
154
|
+
if (!isAlreadyEnvRef(value)) {
|
|
155
|
+
results.push({
|
|
156
|
+
configPath: dotPath,
|
|
157
|
+
key,
|
|
158
|
+
value,
|
|
159
|
+
maskedValue: maskValue(value),
|
|
160
|
+
suggestedEnvVar: configPathToEnvName(dotPath),
|
|
161
|
+
confidence: "medium",
|
|
162
|
+
source: "key-name",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (typeof value === "object" && value !== null) {
|
|
167
|
+
walkConfig(value, dotPath, results);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const isAlreadyEnvRef = (value) =>
|
|
173
|
+
/^\$\{[A-Z_][A-Z0-9_]*\}$/.test(String(value || "").trim());
|
|
174
|
+
|
|
175
|
+
const parseEnvFileSecrets = (content, fileName) => {
|
|
176
|
+
const results = [];
|
|
177
|
+
const lines = String(content || "").split("\n");
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
const trimmed = line.trim();
|
|
180
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
181
|
+
const eqIdx = trimmed.indexOf("=");
|
|
182
|
+
if (eqIdx === -1) continue;
|
|
183
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
184
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
185
|
+
if (!key || !value) continue;
|
|
186
|
+
results.push({
|
|
187
|
+
configPath: `${fileName}:${key}`,
|
|
188
|
+
key,
|
|
189
|
+
value,
|
|
190
|
+
maskedValue: maskValue(value),
|
|
191
|
+
suggestedEnvVar: key,
|
|
192
|
+
confidence: "high",
|
|
193
|
+
source: "env-file",
|
|
194
|
+
fileName,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return results;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const detectSecrets = ({ fs, baseDir, configFiles = [], envFiles = [] }) => {
|
|
201
|
+
const secrets = [];
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
|
|
204
|
+
for (const cfgFile of configFiles) {
|
|
205
|
+
try {
|
|
206
|
+
const fullPath = path.join(baseDir, cfgFile);
|
|
207
|
+
const raw = fs.readFileSync(fullPath, "utf8");
|
|
208
|
+
const cfg = JSON.parse(raw);
|
|
209
|
+
const configSecrets = [];
|
|
210
|
+
walkConfig(cfg, "", configSecrets);
|
|
211
|
+
for (const secret of configSecrets) {
|
|
212
|
+
const dedupeKey = `${secret.suggestedEnvVar}:${secret.value}`;
|
|
213
|
+
if (seen.has(dedupeKey)) continue;
|
|
214
|
+
seen.add(dedupeKey);
|
|
215
|
+
secrets.push({ ...secret, file: cfgFile });
|
|
216
|
+
}
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const envFile of envFiles) {
|
|
221
|
+
try {
|
|
222
|
+
const fullPath = path.join(baseDir, envFile);
|
|
223
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
224
|
+
const envSecrets = parseEnvFileSecrets(content, envFile);
|
|
225
|
+
for (const secret of envSecrets) {
|
|
226
|
+
const dedupeKey = `${secret.suggestedEnvVar}:${secret.value}`;
|
|
227
|
+
if (seen.has(dedupeKey)) {
|
|
228
|
+
const existing = secrets.find(
|
|
229
|
+
(s) => s.suggestedEnvVar === secret.suggestedEnvVar,
|
|
230
|
+
);
|
|
231
|
+
if (existing) {
|
|
232
|
+
existing.duplicateIn = envFile;
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
seen.add(dedupeKey);
|
|
237
|
+
secrets.push({ ...secret, file: envFile });
|
|
238
|
+
}
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return secrets;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const extractPreFillValues = ({ fs, baseDir, configFiles = [] }) => {
|
|
246
|
+
const preFill = {};
|
|
247
|
+
for (const cfgFile of configFiles) {
|
|
248
|
+
try {
|
|
249
|
+
const raw = fs.readFileSync(path.join(baseDir, cfgFile), "utf8");
|
|
250
|
+
const cfg = JSON.parse(raw);
|
|
251
|
+
|
|
252
|
+
if (cfg.models?.active) preFill.MODEL_KEY = cfg.models.active;
|
|
253
|
+
|
|
254
|
+
const providers = cfg.models?.providers || {};
|
|
255
|
+
if (providers.anthropic?.apiKey && !isAlreadyEnvRef(providers.anthropic.apiKey)) {
|
|
256
|
+
preFill.ANTHROPIC_API_KEY = providers.anthropic.apiKey;
|
|
257
|
+
}
|
|
258
|
+
if (providers.openai?.apiKey && !isAlreadyEnvRef(providers.openai.apiKey)) {
|
|
259
|
+
preFill.OPENAI_API_KEY = providers.openai.apiKey;
|
|
260
|
+
}
|
|
261
|
+
if (providers.google?.apiKey && !isAlreadyEnvRef(providers.google.apiKey)) {
|
|
262
|
+
preFill.GEMINI_API_KEY = providers.google.apiKey;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const channels = cfg.channels || {};
|
|
266
|
+
if (channels.telegram?.botToken && !isAlreadyEnvRef(channels.telegram.botToken)) {
|
|
267
|
+
preFill.TELEGRAM_BOT_TOKEN = channels.telegram.botToken;
|
|
268
|
+
}
|
|
269
|
+
if (channels.discord?.token && !isAlreadyEnvRef(channels.discord.token)) {
|
|
270
|
+
preFill.DISCORD_BOT_TOKEN = channels.discord.token;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const braveKey = cfg.tools?.web?.search?.apiKey;
|
|
274
|
+
if (braveKey && !isAlreadyEnvRef(braveKey)) {
|
|
275
|
+
preFill.BRAVE_API_KEY = braveKey;
|
|
276
|
+
}
|
|
277
|
+
} catch {}
|
|
278
|
+
}
|
|
279
|
+
return preFill;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
detectSecrets,
|
|
284
|
+
extractPreFillValues,
|
|
285
|
+
isSensitiveKey,
|
|
286
|
+
matchesValuePrefix,
|
|
287
|
+
maskValue,
|
|
288
|
+
parseEnvFileSecrets,
|
|
289
|
+
};
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const { kSetupDir, kRootDir } = require("../constants");
|
|
3
|
+
const {
|
|
4
|
+
resolveConfigIncludes,
|
|
5
|
+
resolveImportedConfigPaths,
|
|
6
|
+
} = require("./import/import-config");
|
|
3
7
|
const { validateOnboardingInput } = require("./validation");
|
|
4
8
|
const {
|
|
5
9
|
ensureGithubRepoAccessible,
|
|
6
10
|
verifyGithubRepoForOnboarding,
|
|
11
|
+
cloneRepoToTemp,
|
|
7
12
|
} = require("./github");
|
|
8
13
|
const {
|
|
9
14
|
buildOnboardArgs,
|
|
10
15
|
writeSanitizedOpenclawConfig,
|
|
11
16
|
} = require("./openclaw");
|
|
12
17
|
const {
|
|
18
|
+
ensureOpenclawRuntimeArtifacts,
|
|
13
19
|
installControlUiSkill,
|
|
14
20
|
syncBootstrapPromptFiles,
|
|
15
21
|
} = require("./workspace");
|
|
@@ -18,12 +24,148 @@ const {
|
|
|
18
24
|
installHourlyGitSyncCron,
|
|
19
25
|
} = require("./cron");
|
|
20
26
|
const { migrateManagedInternalFiles } = require("../internal-files-migration");
|
|
27
|
+
const { installGogCliSkill } = require("../gog-skill");
|
|
28
|
+
|
|
29
|
+
const kPlaceholderEnvValue = "placeholder";
|
|
30
|
+
const kEnvRefPattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
31
|
+
|
|
32
|
+
const upsertEnvVar = (items, key, value) => {
|
|
33
|
+
const normalizedKey = String(key || "").trim();
|
|
34
|
+
if (!normalizedKey) return items;
|
|
35
|
+
const normalizedValue = String(value || "");
|
|
36
|
+
const existing = items.find((entry) => entry.key === normalizedKey);
|
|
37
|
+
if (existing) {
|
|
38
|
+
existing.value = normalizedValue;
|
|
39
|
+
return items;
|
|
40
|
+
}
|
|
41
|
+
items.push({ key: normalizedKey, value: normalizedValue });
|
|
42
|
+
return items;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const collectEnvRefs = (value, found = new Set()) => {
|
|
46
|
+
if (typeof value === "string") {
|
|
47
|
+
for (const match of value.matchAll(kEnvRefPattern)) {
|
|
48
|
+
found.add(match[1]);
|
|
49
|
+
}
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
value.forEach((entry) => collectEnvRefs(entry, found));
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
if (value && typeof value === "object") {
|
|
57
|
+
Object.values(value).forEach((entry) => collectEnvRefs(entry, found));
|
|
58
|
+
}
|
|
59
|
+
return found;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getEnvVarValue = (items, key) =>
|
|
63
|
+
items.find((entry) => entry.key === key)?.value || "";
|
|
64
|
+
|
|
65
|
+
const buildPlaceholderReview = ({
|
|
66
|
+
referencedEnvVars,
|
|
67
|
+
envVars = [],
|
|
68
|
+
systemVars = new Set(),
|
|
69
|
+
}) => {
|
|
70
|
+
const vars = Array.from(referencedEnvVars)
|
|
71
|
+
.filter((envKey) => !systemVars.has(envKey))
|
|
72
|
+
.sort()
|
|
73
|
+
.map((envKey) => {
|
|
74
|
+
const currentValue = String(getEnvVarValue(envVars, envKey) || "").trim();
|
|
75
|
+
const status =
|
|
76
|
+
currentValue === kPlaceholderEnvValue
|
|
77
|
+
? "placeholder"
|
|
78
|
+
: currentValue
|
|
79
|
+
? "resolved"
|
|
80
|
+
: "missing";
|
|
81
|
+
if (status === "resolved") return null;
|
|
82
|
+
return {
|
|
83
|
+
key: envKey,
|
|
84
|
+
status,
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
return {
|
|
89
|
+
found: vars.length > 0,
|
|
90
|
+
count: vars.length,
|
|
91
|
+
vars,
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const normalizeImportedConfig = ({ fs, openclawDir }) => {
|
|
96
|
+
const configPaths = resolveImportedConfigPaths({ fs, openclawDir });
|
|
97
|
+
for (const configPath of configPaths) {
|
|
98
|
+
let cfg = null;
|
|
99
|
+
try {
|
|
100
|
+
cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
101
|
+
} catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!cfg || typeof cfg !== "object") continue;
|
|
105
|
+
let changed = false;
|
|
106
|
+
const currentToken = String(cfg?.gateway?.auth?.token || "").trim();
|
|
107
|
+
const expectedTokenRef = "${OPENCLAW_GATEWAY_TOKEN}";
|
|
108
|
+
if (cfg.gateway?.auth && currentToken !== expectedTokenRef) {
|
|
109
|
+
cfg.gateway = {
|
|
110
|
+
...(cfg.gateway || {}),
|
|
111
|
+
auth: {
|
|
112
|
+
...(cfg.gateway.auth || {}),
|
|
113
|
+
token: expectedTokenRef,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
changed = true;
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
cfg.hooks &&
|
|
120
|
+
Object.prototype.hasOwnProperty.call(cfg.hooks, "transformsDir")
|
|
121
|
+
) {
|
|
122
|
+
const { transformsDir, ...nextHooks } = cfg.hooks;
|
|
123
|
+
void transformsDir;
|
|
124
|
+
cfg.hooks = nextHooks;
|
|
125
|
+
changed = true;
|
|
126
|
+
}
|
|
127
|
+
if (changed) {
|
|
128
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const getImportedConfigEnvRefs = ({ fs, openclawDir }) => {
|
|
134
|
+
const refs = new Set();
|
|
135
|
+
const configPaths = resolveImportedConfigPaths({ fs, openclawDir });
|
|
136
|
+
for (const configPath of configPaths) {
|
|
137
|
+
try {
|
|
138
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
139
|
+
collectEnvRefs(JSON.parse(raw), refs);
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
142
|
+
return refs;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const getImportedPlaceholderReview = ({
|
|
146
|
+
fs,
|
|
147
|
+
openclawDir,
|
|
148
|
+
envVars = [],
|
|
149
|
+
systemVars = new Set(),
|
|
150
|
+
normalizeConfig = false,
|
|
151
|
+
}) => {
|
|
152
|
+
if (normalizeConfig) {
|
|
153
|
+
normalizeImportedConfig({ fs, openclawDir });
|
|
154
|
+
}
|
|
155
|
+
const referencedEnvVars = getImportedConfigEnvRefs({ fs, openclawDir });
|
|
156
|
+
return buildPlaceholderReview({
|
|
157
|
+
referencedEnvVars,
|
|
158
|
+
envVars,
|
|
159
|
+
systemVars,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
21
162
|
|
|
22
163
|
const createOnboardingService = ({
|
|
23
164
|
fs,
|
|
24
165
|
constants,
|
|
25
166
|
shellCmd,
|
|
26
167
|
gatewayEnv,
|
|
168
|
+
readEnvFile,
|
|
27
169
|
writeEnvFile,
|
|
28
170
|
reloadEnv,
|
|
29
171
|
resolveGithubRepoUrl,
|
|
@@ -39,13 +181,42 @@ const createOnboardingService = ({
|
|
|
39
181
|
const verifyGithubSetup = async ({
|
|
40
182
|
githubRepoInput,
|
|
41
183
|
githubToken,
|
|
184
|
+
mode = "new",
|
|
42
185
|
resolveGithubRepoUrl,
|
|
43
186
|
}) => {
|
|
44
187
|
const repoUrl = resolveGithubRepoUrl(githubRepoInput);
|
|
45
|
-
|
|
188
|
+
const verification = await verifyGithubRepoForOnboarding({
|
|
189
|
+
repoUrl,
|
|
190
|
+
githubToken,
|
|
191
|
+
mode,
|
|
192
|
+
});
|
|
193
|
+
if (!verification.ok) return verification;
|
|
194
|
+
|
|
195
|
+
if (
|
|
196
|
+
mode === "existing" &&
|
|
197
|
+
verification.repoExists &&
|
|
198
|
+
!verification.repoIsEmpty
|
|
199
|
+
) {
|
|
200
|
+
const cloneResult = await cloneRepoToTemp({
|
|
201
|
+
repoUrl,
|
|
202
|
+
githubToken,
|
|
203
|
+
shellCmd,
|
|
204
|
+
});
|
|
205
|
+
if (!cloneResult.ok) {
|
|
206
|
+
return { ok: false, status: 400, error: cloneResult.error };
|
|
207
|
+
}
|
|
208
|
+
return { ...verification, tempDir: cloneResult.tempDir };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return verification;
|
|
46
212
|
};
|
|
47
213
|
|
|
48
|
-
const completeOnboarding = async ({
|
|
214
|
+
const completeOnboarding = async ({
|
|
215
|
+
req,
|
|
216
|
+
vars,
|
|
217
|
+
modelKey,
|
|
218
|
+
importMode = false,
|
|
219
|
+
}) => {
|
|
49
220
|
const validation = validateOnboardingInput({
|
|
50
221
|
vars,
|
|
51
222
|
modelKey,
|
|
@@ -68,14 +239,37 @@ const createOnboardingService = ({
|
|
|
68
239
|
} = validation.data;
|
|
69
240
|
|
|
70
241
|
const repoUrl = resolveGithubRepoUrl(githubRepoInput);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
242
|
+
const remoteUrl = `https://github.com/${repoUrl}.git`;
|
|
243
|
+
const existingConfigPresent =
|
|
244
|
+
importMode && fs.existsSync(`${OPENCLAW_DIR}/openclaw.json`);
|
|
245
|
+
const existingEnvVars =
|
|
246
|
+
typeof readEnvFile === "function" ? readEnvFile() : [];
|
|
247
|
+
const varsToSave = [...existingEnvVars];
|
|
248
|
+
for (const entry of vars.filter(
|
|
249
|
+
(item) => item.value && item.key !== "GITHUB_WORKSPACE_REPO",
|
|
250
|
+
)) {
|
|
251
|
+
upsertEnvVar(varsToSave, entry.key, entry.value);
|
|
252
|
+
}
|
|
253
|
+
upsertEnvVar(varsToSave, "GITHUB_WORKSPACE_REPO", repoUrl);
|
|
254
|
+
if (importMode && existingConfigPresent) {
|
|
255
|
+
const systemVars =
|
|
256
|
+
constants.kSystemVars instanceof Set
|
|
257
|
+
? constants.kSystemVars
|
|
258
|
+
: new Set();
|
|
259
|
+
const placeholderReview = getImportedPlaceholderReview({
|
|
260
|
+
fs,
|
|
261
|
+
openclawDir: OPENCLAW_DIR,
|
|
262
|
+
envVars: varsToSave,
|
|
263
|
+
systemVars,
|
|
264
|
+
normalizeConfig: true,
|
|
265
|
+
});
|
|
266
|
+
for (const placeholderVar of placeholderReview.vars) {
|
|
267
|
+
upsertEnvVar(varsToSave, placeholderVar.key, kPlaceholderEnvValue);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
75
270
|
writeEnvFile(varsToSave);
|
|
76
271
|
reloadEnv();
|
|
77
272
|
|
|
78
|
-
const remoteUrl = `https://github.com/${repoUrl}.git`;
|
|
79
273
|
const [, repoName] = repoUrl.split("/");
|
|
80
274
|
const repoCheck = await ensureGithubRepoAccessible({
|
|
81
275
|
repoUrl,
|
|
@@ -100,12 +294,30 @@ const createOnboardingService = ({
|
|
|
100
294
|
workspaceDir: WORKSPACE_DIR,
|
|
101
295
|
baseUrl: getBaseUrl(req),
|
|
102
296
|
});
|
|
297
|
+
ensureOpenclawRuntimeArtifacts({
|
|
298
|
+
fs,
|
|
299
|
+
openclawDir: OPENCLAW_DIR,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const hadImportedGit = importMode && fs.existsSync(`${OPENCLAW_DIR}/.git`);
|
|
303
|
+
if (hadImportedGit) {
|
|
304
|
+
try {
|
|
305
|
+
fs.rmSync(`${OPENCLAW_DIR}/.git`, { recursive: true, force: true });
|
|
306
|
+
} catch {}
|
|
307
|
+
}
|
|
103
308
|
|
|
104
|
-
if (!fs.existsSync(`${OPENCLAW_DIR}/.git`)) {
|
|
309
|
+
if (hadImportedGit || !fs.existsSync(`${OPENCLAW_DIR}/.git`)) {
|
|
105
310
|
await shellCmd(
|
|
106
311
|
`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"`,
|
|
107
312
|
);
|
|
108
313
|
console.log("[onboard] Git initialized");
|
|
314
|
+
} else if (importMode) {
|
|
315
|
+
// Ensure remote points to the correct URL for imported repos
|
|
316
|
+
try {
|
|
317
|
+
await shellCmd(
|
|
318
|
+
`cd ${OPENCLAW_DIR} && git remote set-url origin "${remoteUrl}" && git config user.email "agent@alphaclaw.md" && git config user.name "AlphaClaw Agent"`,
|
|
319
|
+
);
|
|
320
|
+
} catch {}
|
|
109
321
|
}
|
|
110
322
|
|
|
111
323
|
if (!fs.existsSync(`${OPENCLAW_DIR}/.gitignore`)) {
|
|
@@ -115,24 +327,30 @@ const createOnboardingService = ({
|
|
|
115
327
|
);
|
|
116
328
|
}
|
|
117
329
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
330
|
+
if (!existingConfigPresent) {
|
|
331
|
+
const onboardArgs = buildOnboardArgs({
|
|
332
|
+
varMap,
|
|
333
|
+
selectedProvider,
|
|
334
|
+
hasCodexOauth,
|
|
335
|
+
workspaceDir: WORKSPACE_DIR,
|
|
336
|
+
});
|
|
337
|
+
await shellCmd(
|
|
338
|
+
`openclaw onboard ${onboardArgs.map((a) => `"${a}"`).join(" ")}`,
|
|
339
|
+
{
|
|
340
|
+
env: {
|
|
341
|
+
...process.env,
|
|
342
|
+
OPENCLAW_HOME: kRootDir,
|
|
343
|
+
OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
|
|
344
|
+
},
|
|
345
|
+
timeout: 120000,
|
|
131
346
|
},
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
347
|
+
);
|
|
348
|
+
console.log("[onboard] Onboard complete");
|
|
349
|
+
} else {
|
|
350
|
+
console.log(
|
|
351
|
+
"[onboard] Skipped openclaw onboard (existing config present)",
|
|
352
|
+
);
|
|
353
|
+
}
|
|
136
354
|
|
|
137
355
|
await shellCmd(`openclaw models set "${modelKey}"`, {
|
|
138
356
|
env: gatewayEnv(),
|
|
@@ -148,7 +366,9 @@ const createOnboardingService = ({
|
|
|
148
366
|
fs.rmSync(`${WORKSPACE_DIR}/.git`, { recursive: true, force: true });
|
|
149
367
|
} catch {}
|
|
150
368
|
|
|
151
|
-
|
|
369
|
+
if (!existingConfigPresent) {
|
|
370
|
+
writeSanitizedOpenclawConfig({ fs, openclawDir: OPENCLAW_DIR, varMap });
|
|
371
|
+
}
|
|
152
372
|
authProfiles?.syncConfigAuthReferencesForAgent?.();
|
|
153
373
|
ensureGatewayProxyConfig(getBaseUrl(req));
|
|
154
374
|
|
|
@@ -157,6 +377,7 @@ const createOnboardingService = ({
|
|
|
157
377
|
openclawDir: OPENCLAW_DIR,
|
|
158
378
|
baseUrl: getBaseUrl(req),
|
|
159
379
|
});
|
|
380
|
+
installGogCliSkill({ fs, openclawDir: OPENCLAW_DIR });
|
|
160
381
|
|
|
161
382
|
installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
|
|
162
383
|
await installHourlyGitSyncCron({ fs, openclawDir: OPENCLAW_DIR });
|
|
@@ -166,7 +387,7 @@ const createOnboardingService = ({
|
|
|
166
387
|
JSON.stringify(
|
|
167
388
|
{
|
|
168
389
|
onboarded: true,
|
|
169
|
-
reason: "onboarding_complete",
|
|
390
|
+
reason: importMode ? "import_complete" : "onboarding_complete",
|
|
170
391
|
markedAt: new Date().toISOString(),
|
|
171
392
|
},
|
|
172
393
|
null,
|
|
@@ -175,7 +396,10 @@ const createOnboardingService = ({
|
|
|
175
396
|
);
|
|
176
397
|
|
|
177
398
|
try {
|
|
178
|
-
|
|
399
|
+
const commitMsg = importMode
|
|
400
|
+
? "imported existing setup via AlphaClaw"
|
|
401
|
+
: "initial setup";
|
|
402
|
+
await shellCmd(`alphaclaw git-sync -m "${commitMsg}"`, {
|
|
179
403
|
timeout: 30000,
|
|
180
404
|
env: {
|
|
181
405
|
...process.env,
|
|
@@ -194,4 +418,7 @@ const createOnboardingService = ({
|
|
|
194
418
|
return { completeOnboarding, verifyGithubSetup };
|
|
195
419
|
};
|
|
196
420
|
|
|
197
|
-
module.exports = {
|
|
421
|
+
module.exports = {
|
|
422
|
+
createOnboardingService,
|
|
423
|
+
getImportedPlaceholderReview,
|
|
424
|
+
};
|