@chrysb/alphaclaw 0.4.6-beta.8 → 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 (28) 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/envars.js +0 -1
  5. package/lib/public/js/components/onboarding/welcome-config.js +39 -17
  6. package/lib/public/js/components/onboarding/welcome-form-step.js +142 -47
  7. package/lib/public/js/components/onboarding/welcome-import-step.js +306 -0
  8. package/lib/public/js/components/onboarding/welcome-placeholder-review-step.js +99 -0
  9. package/lib/public/js/components/onboarding/welcome-secret-review-step.js +191 -0
  10. package/lib/public/js/components/segmented-control.js +7 -1
  11. package/lib/public/js/components/welcome/index.js +112 -0
  12. package/lib/public/js/components/welcome/use-welcome.js +561 -0
  13. package/lib/public/js/lib/api.js +221 -161
  14. package/lib/server/commands.js +1 -0
  15. package/lib/server/constants.js +0 -1
  16. package/lib/server/gateway.js +15 -40
  17. package/lib/server/onboarding/github.js +120 -19
  18. package/lib/server/onboarding/import/import-applier.js +321 -0
  19. package/lib/server/onboarding/import/import-config.js +69 -0
  20. package/lib/server/onboarding/import/import-scanner.js +469 -0
  21. package/lib/server/onboarding/import/import-temp.js +63 -0
  22. package/lib/server/onboarding/import/secret-detector.js +289 -0
  23. package/lib/server/onboarding/index.js +256 -29
  24. package/lib/server/onboarding/workspace.js +38 -6
  25. package/lib/server/routes/onboarding.js +281 -12
  26. package/lib/server.js +11 -2
  27. package/package.json +1 -1
  28. package/lib/public/js/components/welcome.js +0 -318
@@ -1,5 +1,9 @@
1
1
  const path = require("path");
2
- const { kSetupDir, OPENCLAW_DIR } = require("../constants");
2
+ const {
3
+ kSetupDir,
4
+ OPENCLAW_DIR,
5
+ ENV_FILE_PATH,
6
+ } = require("../constants");
3
7
  const { renderTopicRegistryMarkdown } = require("../topic-registry");
4
8
  const { readGoogleState } = require("../google-state");
5
9
 
@@ -71,11 +75,19 @@ const renderGoogleAccountsMarkdown = (fs) => {
71
75
  const syncBootstrapPromptFiles = ({ fs, workspaceDir, baseUrl }) => {
72
76
  try {
73
77
  const setupUiUrl = resolveSetupUiUrl(baseUrl);
74
- const bootstrapDir = `${workspaceDir}/hooks/bootstrap`;
78
+ const bootstrapDir = path.join(workspaceDir, "hooks", "bootstrap");
75
79
  fs.mkdirSync(bootstrapDir, { recursive: true });
76
- fs.copyFileSync(path.join(kSetupDir, "core-prompts", "AGENTS.md"), `${bootstrapDir}/AGENTS.md`);
77
80
 
78
- const toolsTemplate = fs.readFileSync(path.join(kSetupDir, "core-prompts", "TOOLS.md"), "utf8");
81
+ // AlphaClaw-managed files are always overwritten (even during import)
82
+ fs.copyFileSync(
83
+ path.join(kSetupDir, "core-prompts", "AGENTS.md"),
84
+ path.join(bootstrapDir, "AGENTS.md"),
85
+ );
86
+
87
+ const toolsTemplate = fs.readFileSync(
88
+ path.join(kSetupDir, "core-prompts", "TOOLS.md"),
89
+ "utf8",
90
+ );
79
91
  let toolsContent = toolsTemplate.replace(
80
92
  /\{\{SETUP_UI_URL\}\}/g,
81
93
  setupUiUrl,
@@ -92,7 +104,7 @@ const syncBootstrapPromptFiles = ({ fs, workspaceDir, baseUrl }) => {
92
104
  toolsContent += googleAccountsSection;
93
105
  }
94
106
 
95
- fs.writeFileSync(`${bootstrapDir}/TOOLS.md`, toolsContent);
107
+ fs.writeFileSync(path.join(bootstrapDir, "TOOLS.md"), toolsContent);
96
108
  console.log("[onboard] Bootstrap prompt files synced");
97
109
  } catch (e) {
98
110
  console.error("[onboard] Bootstrap prompt sync error:", e.message);
@@ -113,4 +125,24 @@ const installControlUiSkill = ({ fs, openclawDir, baseUrl }) => {
113
125
  }
114
126
  };
115
127
 
116
- module.exports = { installControlUiSkill, syncBootstrapPromptFiles };
128
+ const ensureOpenclawRuntimeArtifacts = ({
129
+ fs,
130
+ openclawDir,
131
+ envFilePath = ENV_FILE_PATH,
132
+ }) => {
133
+ try {
134
+ const openclawEnvLink = path.join(openclawDir, ".env");
135
+ if (!fs.existsSync(openclawEnvLink) && fs.existsSync(envFilePath)) {
136
+ fs.symlinkSync(envFilePath, openclawEnvLink);
137
+ console.log(`[alphaclaw] Symlinked ${openclawEnvLink} -> ${envFilePath}`);
138
+ }
139
+ } catch (e) {
140
+ console.log(`[alphaclaw] .env symlink skipped: ${e.message}`);
141
+ }
142
+ };
143
+
144
+ module.exports = {
145
+ ensureOpenclawRuntimeArtifacts,
146
+ installControlUiSkill,
147
+ syncBootstrapPromptFiles,
148
+ };
@@ -1,4 +1,20 @@
1
- const { createOnboardingService } = require("../onboarding");
1
+ const {
2
+ createOnboardingService,
3
+ getImportedPlaceholderReview,
4
+ } = require("../onboarding");
5
+ const path = require("path");
6
+ const { scanWorkspace } = require("../onboarding/import/import-scanner");
7
+ const {
8
+ detectSecrets,
9
+ extractPreFillValues,
10
+ } = require("../onboarding/import/secret-detector");
11
+ const {
12
+ promoteCloneToTarget,
13
+ alignHookTransforms,
14
+ applySecretExtraction,
15
+ isValidTempDir,
16
+ } = require("../onboarding/import/import-applier");
17
+ const { cleanupTempClone } = require("../onboarding/github");
2
18
 
3
19
  const sanitizeOnboardingError = (error) => {
4
20
  const raw = [error?.stderr, error?.stdout, error?.message]
@@ -7,6 +23,7 @@ const sanitizeOnboardingError = (error) => {
7
23
  const redacted = String(raw || "Onboarding failed")
8
24
  .replace(/sk-[^\s"]+/g, "***")
9
25
  .replace(/ghp_[^\s"]+/g, "***")
26
+ .replace(/github_pat_[^\s"]+/g, "***")
10
27
  .replace(/(?:token|api[_-]?key)["'\s:=]+[^\s"']+/gi, (match) =>
11
28
  match.replace(/[^\s"':=]+$/g, "***"),
12
29
  );
@@ -65,6 +82,7 @@ const registerOnboardingRoutes = ({
65
82
  constants,
66
83
  shellCmd,
67
84
  gatewayEnv,
85
+ readEnvFile,
68
86
  writeEnvFile,
69
87
  reloadEnv,
70
88
  isOnboarded,
@@ -76,11 +94,17 @@ const registerOnboardingRoutes = ({
76
94
  getBaseUrl,
77
95
  startGateway,
78
96
  }) => {
97
+ // Keep mutating onboarding routes marker-gated so in-progress imports
98
+ // can promote files before the final completion marker is written.
99
+ const hasExplicitOnboardingMarker = () =>
100
+ fs.existsSync(constants.kOnboardingMarkerPath);
101
+
79
102
  const onboardingService = createOnboardingService({
80
103
  fs,
81
104
  constants,
82
105
  shellCmd,
83
106
  gatewayEnv,
107
+ readEnvFile,
84
108
  writeEnvFile,
85
109
  reloadEnv,
86
110
  resolveGithubRepoUrl,
@@ -92,20 +116,57 @@ const registerOnboardingRoutes = ({
92
116
  startGateway,
93
117
  });
94
118
 
119
+ const kEnvVarNamePattern = /^[A-Z_][A-Z0-9_]*$/;
120
+ const validateApprovedSecrets = ({ approvedSecrets = [], scannedSecrets = [] }) => {
121
+ if (!Array.isArray(approvedSecrets)) return { ok: true, secrets: [] };
122
+ const scannedByFingerprint = new Map(
123
+ scannedSecrets.map((secret) => [
124
+ [
125
+ String(secret?.configPath || ""),
126
+ String(secret?.file || ""),
127
+ String(secret?.value || ""),
128
+ ].join("\u0000"),
129
+ secret,
130
+ ]),
131
+ );
132
+ const secrets = [];
133
+ for (const approvedSecret of approvedSecrets) {
134
+ const fingerprint = [
135
+ String(approvedSecret?.configPath || ""),
136
+ String(approvedSecret?.file || ""),
137
+ String(approvedSecret?.value || ""),
138
+ ].join("\u0000");
139
+ const scannedSecret = scannedByFingerprint.get(fingerprint);
140
+ const envVarName = String(approvedSecret?.suggestedEnvVar || "").trim();
141
+ if (!scannedSecret || !envVarName || !kEnvVarNamePattern.test(envVarName)) {
142
+ return {
143
+ ok: false,
144
+ error: "Invalid approved secrets payload",
145
+ };
146
+ }
147
+ secrets.push({
148
+ ...scannedSecret,
149
+ suggestedEnvVar: envVarName,
150
+ });
151
+ }
152
+ return { ok: true, secrets };
153
+ };
154
+
95
155
  app.get("/api/onboard/status", (req, res) => {
96
- res.json({ onboarded: isOnboarded() });
156
+ res.json({ onboarded: hasExplicitOnboardingMarker() });
97
157
  });
98
158
 
99
159
  app.post("/api/onboard", async (req, res) => {
100
- if (isOnboarded())
160
+ if (hasExplicitOnboardingMarker())
101
161
  return res.json({ ok: false, error: "Already onboarded" });
102
162
 
103
163
  try {
104
- const { vars, modelKey } = req.body;
164
+ const { vars, modelKey, importMode } = req.body;
105
165
  const result = await onboardingService.completeOnboarding({
106
166
  req,
107
167
  vars,
108
168
  modelKey,
169
+ importMode: !!importMode,
109
170
  });
110
171
  res.status(result.status).json(result.body);
111
172
  } catch (err) {
@@ -115,25 +176,25 @@ const registerOnboardingRoutes = ({
115
176
  });
116
177
 
117
178
  app.post("/api/onboard/github/verify", async (req, res) => {
118
- if (isOnboarded()) {
179
+ if (hasExplicitOnboardingMarker()) {
119
180
  return res.json({ ok: false, error: "Already onboarded" });
120
181
  }
121
182
 
122
183
  try {
123
184
  const githubRepoInput = String(req.body?.repo || "").trim();
124
185
  const githubToken = String(req.body?.token || "").trim();
186
+ const mode = String(req.body?.mode || "new").trim();
125
187
  if (!githubRepoInput || !githubToken) {
126
- return res
127
- .status(400)
128
- .json({
129
- ok: false,
130
- error: "GitHub token and workspace repo are required",
131
- });
188
+ return res.status(400).json({
189
+ ok: false,
190
+ error: "GitHub token and workspace repo are required",
191
+ });
132
192
  }
133
193
 
134
194
  const result = await onboardingService.verifyGithubSetup({
135
195
  githubRepoInput,
136
196
  githubToken,
197
+ mode,
137
198
  resolveGithubRepoUrl,
138
199
  });
139
200
  if (!result.ok) {
@@ -141,7 +202,12 @@ const registerOnboardingRoutes = ({
141
202
  .status(result.status || 400)
142
203
  .json({ ok: false, error: result.error });
143
204
  }
144
- return res.json({ ok: true });
205
+ return res.json({
206
+ ok: true,
207
+ repoExists: result.repoExists || false,
208
+ repoIsEmpty: result.repoIsEmpty || false,
209
+ tempDir: result.tempDir || null,
210
+ });
145
211
  } catch (err) {
146
212
  console.error("[onboard] GitHub verify error:", err);
147
213
  return res
@@ -149,6 +215,209 @@ const registerOnboardingRoutes = ({
149
215
  .json({ ok: false, error: sanitizeOnboardingError(err) });
150
216
  }
151
217
  });
218
+ app.post("/api/onboard/import/scan", async (req, res) => {
219
+ if (hasExplicitOnboardingMarker()) {
220
+ return res.json({ ok: false, error: "Already onboarded" });
221
+ }
222
+
223
+ try {
224
+ const tempDir = String(req.body?.tempDir || "").trim();
225
+ if (!tempDir || !isValidTempDir(tempDir)) {
226
+ return res
227
+ .status(400)
228
+ .json({ ok: false, error: "Invalid temp directory" });
229
+ }
230
+ if (!fs.existsSync(tempDir)) {
231
+ return res
232
+ .status(400)
233
+ .json({ ok: false, error: "Temp directory not found" });
234
+ }
235
+
236
+ const scan = scanWorkspace({ fs, baseDir: tempDir });
237
+ if (!scan.sourceLayout?.supported) {
238
+ cleanupTempClone(tempDir);
239
+ return res.status(400).json({
240
+ ok: false,
241
+ error: scan.sourceLayout?.error || "Unsupported import source layout",
242
+ });
243
+ }
244
+
245
+ const secrets = detectSecrets({
246
+ fs,
247
+ baseDir: tempDir,
248
+ configFiles: scan.gatewayConfig.files,
249
+ envFiles: scan.envFiles.files,
250
+ });
251
+ const preFill = extractPreFillValues({
252
+ fs,
253
+ baseDir: tempDir,
254
+ configFiles: scan.gatewayConfig.files,
255
+ });
256
+
257
+ return res.json({ ok: true, ...scan, secrets, preFill });
258
+ } catch (err) {
259
+ console.error("[onboard] Import scan error:", err);
260
+ return res
261
+ .status(500)
262
+ .json({ ok: false, error: sanitizeOnboardingError(err) });
263
+ }
264
+ });
265
+
266
+ app.post("/api/onboard/import/apply", async (req, res) => {
267
+ if (hasExplicitOnboardingMarker()) {
268
+ return res.json({ ok: false, error: "Already onboarded" });
269
+ }
270
+
271
+ try {
272
+ const tempDir = String(req.body?.tempDir || "").trim();
273
+ const approvedSecrets = Array.isArray(req.body?.approvedSecrets)
274
+ ? req.body.approvedSecrets
275
+ : [];
276
+ const skipSecretExtraction = !!req.body?.skipSecretExtraction;
277
+ const githubToken = String(req.body?.githubToken || "").trim();
278
+ const githubRepoInput = String(req.body?.githubRepo || "").trim();
279
+
280
+ if (!tempDir || !isValidTempDir(tempDir)) {
281
+ return res
282
+ .status(400)
283
+ .json({ ok: false, error: "Invalid temp directory" });
284
+ }
285
+ if (!fs.existsSync(tempDir)) {
286
+ return res
287
+ .status(400)
288
+ .json({ ok: false, error: "Temp directory not found" });
289
+ }
290
+
291
+ const scan = scanWorkspace({ fs, baseDir: tempDir });
292
+ if (!scan.sourceLayout?.supported) {
293
+ cleanupTempClone(tempDir);
294
+ return res.status(400).json({
295
+ ok: false,
296
+ error: scan.sourceLayout?.error || "Unsupported import source layout",
297
+ });
298
+ }
299
+
300
+ let envVars = [];
301
+ const scannedSecrets = detectSecrets({
302
+ fs,
303
+ baseDir: tempDir,
304
+ configFiles: scan.gatewayConfig.files,
305
+ envFiles: scan.envFiles.files,
306
+ });
307
+ const approvedSecretValidation = validateApprovedSecrets({
308
+ approvedSecrets,
309
+ scannedSecrets,
310
+ });
311
+ if (!approvedSecretValidation.ok) {
312
+ return res.status(400).json({
313
+ ok: false,
314
+ error: approvedSecretValidation.error,
315
+ });
316
+ }
317
+ if (!skipSecretExtraction && approvedSecrets.length > 0) {
318
+ const extraction = applySecretExtraction({
319
+ fs,
320
+ baseDir: tempDir,
321
+ approvedSecrets: approvedSecretValidation.secrets,
322
+ });
323
+ envVars = extraction.envVars;
324
+ }
325
+
326
+ const configFiles = ["openclaw.json"].filter((f) =>
327
+ fs.existsSync(path.join(tempDir, f)),
328
+ );
329
+ const transformAlignment = alignHookTransforms({
330
+ fs,
331
+ baseDir: tempDir,
332
+ configFiles,
333
+ });
334
+
335
+ const preFill = extractPreFillValues({
336
+ fs,
337
+ baseDir: tempDir,
338
+ configFiles,
339
+ });
340
+
341
+ const promoteTargetDir =
342
+ scan.sourceLayout.kind === "workspace-only"
343
+ ? constants.WORKSPACE_DIR
344
+ : constants.OPENCLAW_DIR;
345
+ const promoteResult = promoteCloneToTarget({
346
+ fs,
347
+ tempDir,
348
+ targetDir: promoteTargetDir,
349
+ sourceSubdir: scan.sourceLayout.promoteSourceSubdir || "",
350
+ cleanupBootstrap: scan.sourceLayout.kind === "full-openclaw-root",
351
+ });
352
+ if (!promoteResult.ok) {
353
+ return res.status(500).json({ ok: false, error: promoteResult.error });
354
+ }
355
+
356
+ const existing = typeof readEnvFile === "function" ? readEnvFile() : [];
357
+ const merged = [...existing];
358
+ if (githubToken) {
359
+ const tokenIdx = merged.findIndex((v) => v.key === "GITHUB_TOKEN");
360
+ if (tokenIdx >= 0) {
361
+ merged[tokenIdx] = { key: "GITHUB_TOKEN", value: githubToken };
362
+ } else {
363
+ merged.push({ key: "GITHUB_TOKEN", value: githubToken });
364
+ }
365
+ }
366
+ if (githubRepoInput) {
367
+ const normalizedRepo = resolveGithubRepoUrl(githubRepoInput);
368
+ const repoIdx = merged.findIndex(
369
+ (v) => v.key === "GITHUB_WORKSPACE_REPO",
370
+ );
371
+ if (repoIdx >= 0) {
372
+ merged[repoIdx] = {
373
+ key: "GITHUB_WORKSPACE_REPO",
374
+ value: normalizedRepo,
375
+ };
376
+ } else {
377
+ merged.push({
378
+ key: "GITHUB_WORKSPACE_REPO",
379
+ value: normalizedRepo,
380
+ });
381
+ }
382
+ }
383
+ for (const newVar of envVars) {
384
+ const idx = merged.findIndex((v) => v.key === newVar.key);
385
+ if (idx >= 0) {
386
+ merged[idx] = newVar;
387
+ } else {
388
+ merged.push(newVar);
389
+ }
390
+ }
391
+ if (githubToken || githubRepoInput || envVars.length > 0) {
392
+ writeEnvFile(merged);
393
+ reloadEnv();
394
+ }
395
+ const systemVars =
396
+ constants.kSystemVars instanceof Set ? constants.kSystemVars : new Set();
397
+ const placeholderReview = getImportedPlaceholderReview({
398
+ fs,
399
+ openclawDir: constants.OPENCLAW_DIR,
400
+ envVars: merged,
401
+ systemVars,
402
+ normalizeConfig: true,
403
+ });
404
+
405
+ return res.json({
406
+ ok: true,
407
+ preFill,
408
+ placeholderReview,
409
+ sourceLayout: scan.sourceLayout,
410
+ envVarsImported: envVars.length,
411
+ transformsAligned: transformAlignment.alignedCount,
412
+ });
413
+ } catch (err) {
414
+ console.error("[onboard] Import apply error:", err);
415
+ cleanupTempClone(req.body?.tempDir);
416
+ return res
417
+ .status(500)
418
+ .json({ ok: false, error: sanitizeOnboardingError(err) });
419
+ }
420
+ });
152
421
  };
153
422
 
154
423
  module.exports = { registerOnboardingRoutes };
package/lib/server.js CHANGED
@@ -88,9 +88,13 @@ const {
88
88
  createRestartRequiredState,
89
89
  } = require("./server/restart-required-state");
90
90
  const {
91
+ ensureOpenclawRuntimeArtifacts,
91
92
  installControlUiSkill,
92
93
  syncBootstrapPromptFiles,
93
94
  } = require("./server/onboarding/workspace");
95
+ const {
96
+ cleanupStaleImportTempDirs,
97
+ } = require("./server/onboarding/import/import-temp");
94
98
  const {
95
99
  migrateManagedInternalFiles,
96
100
  } = require("./server/internal-files-migration");
@@ -122,6 +126,7 @@ const { PORT, GATEWAY_URL, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
122
126
 
123
127
  startEnvWatcher();
124
128
  attachGatewaySignalHandlers();
129
+ cleanupStaleImportTempDirs();
125
130
  migrateManagedInternalFiles({
126
131
  fs,
127
132
  openclawDir: constants.OPENCLAW_DIR,
@@ -214,6 +219,7 @@ registerOnboardingRoutes({
214
219
  constants,
215
220
  shellCmd,
216
221
  gatewayEnv,
222
+ readEnvFile,
217
223
  writeEnvFile,
218
224
  reloadEnv,
219
225
  isOnboarded,
@@ -317,6 +323,10 @@ setGatewayExitHandler((payload) => watchdog.onGatewayExit(payload));
317
323
  setGatewayLaunchHandler((payload) => watchdog.onGatewayLaunch(payload));
318
324
  const doSyncPromptFiles = () => {
319
325
  const setupUiUrl = resolveSetupUrl();
326
+ ensureOpenclawRuntimeArtifacts({
327
+ fs,
328
+ openclawDir: constants.OPENCLAW_DIR,
329
+ });
320
330
  syncBootstrapPromptFiles({
321
331
  fs,
322
332
  workspaceDir: constants.WORKSPACE_DIR,
@@ -329,7 +339,6 @@ const doSyncPromptFiles = () => {
329
339
  });
330
340
  installGogCliSkill({ fs, openclawDir: constants.OPENCLAW_DIR });
331
341
  };
332
- doSyncPromptFiles();
333
342
  registerTelegramRoutes({
334
343
  app,
335
344
  telegramApi,
@@ -403,8 +412,8 @@ server.on("upgrade", (req, socket, head) => {
403
412
 
404
413
  server.listen(PORT, "0.0.0.0", () => {
405
414
  console.log(`[alphaclaw] Express listening on :${PORT}`);
406
- doSyncPromptFiles();
407
415
  if (isOnboarded()) {
416
+ doSyncPromptFiles();
408
417
  reloadEnv();
409
418
  syncChannelConfig(readEnvFile());
410
419
  ensureGatewayProxyConfig(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.4.6-beta.8",
3
+ "version": "0.4.6-beta.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },