@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.
Files changed (33) hide show
  1. package/bin/alphaclaw.js +2 -32
  2. package/lib/public/css/theme.css +19 -0
  3. package/lib/public/js/app.js +1 -1
  4. package/lib/public/js/components/doctor/helpers.js +71 -5
  5. package/lib/public/js/components/doctor/index.js +89 -28
  6. package/lib/public/js/components/envars.js +0 -1
  7. package/lib/public/js/components/onboarding/welcome-config.js +39 -17
  8. package/lib/public/js/components/onboarding/welcome-form-step.js +142 -47
  9. package/lib/public/js/components/onboarding/welcome-import-step.js +306 -0
  10. package/lib/public/js/components/onboarding/welcome-placeholder-review-step.js +99 -0
  11. package/lib/public/js/components/onboarding/welcome-secret-review-step.js +191 -0
  12. package/lib/public/js/components/segmented-control.js +7 -1
  13. package/lib/public/js/components/welcome/index.js +112 -0
  14. package/lib/public/js/components/welcome/use-welcome.js +561 -0
  15. package/lib/public/js/lib/api.js +221 -161
  16. package/lib/server/commands.js +1 -0
  17. package/lib/server/constants.js +0 -1
  18. package/lib/server/doctor/bootstrap-context.js +191 -0
  19. package/lib/server/doctor/prompt.js +20 -4
  20. package/lib/server/doctor/service.js +18 -4
  21. package/lib/server/gateway.js +15 -40
  22. package/lib/server/onboarding/github.js +120 -19
  23. package/lib/server/onboarding/import/import-applier.js +321 -0
  24. package/lib/server/onboarding/import/import-config.js +69 -0
  25. package/lib/server/onboarding/import/import-scanner.js +469 -0
  26. package/lib/server/onboarding/import/import-temp.js +63 -0
  27. package/lib/server/onboarding/import/secret-detector.js +289 -0
  28. package/lib/server/onboarding/index.js +256 -29
  29. package/lib/server/onboarding/workspace.js +38 -6
  30. package/lib/server/routes/onboarding.js +281 -12
  31. package/lib/server.js +12 -3
  32. package/package.json +1 -1
  33. 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
- return verifyGithubRepoForOnboarding({ repoUrl, githubToken });
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 ({ req, vars, modelKey }) => {
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 varsToSave = [
72
- ...vars.filter((v) => v.value && v.key !== "GITHUB_WORKSPACE_REPO"),
73
- ];
74
- varsToSave.push({ key: "GITHUB_WORKSPACE_REPO", value: repoUrl });
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
- const onboardArgs = buildOnboardArgs({
119
- varMap,
120
- selectedProvider,
121
- hasCodexOauth,
122
- workspaceDir: WORKSPACE_DIR,
123
- });
124
- await shellCmd(
125
- `openclaw onboard ${onboardArgs.map((a) => `"${a}"`).join(" ")}`,
126
- {
127
- env: {
128
- ...process.env,
129
- OPENCLAW_HOME: kRootDir,
130
- OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
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
- timeout: 120000,
133
- },
134
- );
135
- console.log("[onboard] Onboard complete");
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
- writeSanitizedOpenclawConfig({ fs, openclawDir: OPENCLAW_DIR, varMap });
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
- await shellCmd(`alphaclaw git-sync -m "initial setup"`, {
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 = { createOnboardingService };
421
+ module.exports = {
422
+ createOnboardingService,
423
+ getImportedPlaceholderReview,
424
+ };