@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
package/bin/alphaclaw.js CHANGED
@@ -216,21 +216,7 @@ try {
216
216
  }
217
217
 
218
218
  // ---------------------------------------------------------------------------
219
- // 5. Symlink <rootDir>/.openclaw/.env -> <rootDir>/.env
220
- // ---------------------------------------------------------------------------
221
-
222
- const openclawEnvLink = path.join(openclawDir, ".env");
223
- try {
224
- if (!fs.existsSync(openclawEnvLink)) {
225
- fs.symlinkSync(envFilePath, openclawEnvLink);
226
- console.log(`[alphaclaw] Symlinked ${openclawEnvLink} -> ${envFilePath}`);
227
- }
228
- } catch (e) {
229
- console.log(`[alphaclaw] .env symlink skipped: ${e.message}`);
230
- }
231
-
232
- // ---------------------------------------------------------------------------
233
- // 6. Load .env into process.env
219
+ // 5. Load .env into process.env
234
220
  // ---------------------------------------------------------------------------
235
221
 
236
222
  if (fs.existsSync(envFilePath)) {
@@ -530,23 +516,7 @@ if (!gogInstalled) {
530
516
  }
531
517
 
532
518
  // ---------------------------------------------------------------------------
533
- // 7. Configure gog keyring (file backend for headless environments)
534
- // ---------------------------------------------------------------------------
535
-
536
- process.env.GOG_KEYRING_PASSWORD =
537
- process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
538
- const gogConfigFile = path.join(openclawDir, "gogcli", "config.json");
539
-
540
- if (!fs.existsSync(gogConfigFile)) {
541
- fs.mkdirSync(path.join(openclawDir, "gogcli"), { recursive: true });
542
- try {
543
- execSync("gog auth keyring file", { stdio: "ignore" });
544
- console.log("[alphaclaw] gog keyring configured (file backend)");
545
- } catch {}
546
- }
547
-
548
- // ---------------------------------------------------------------------------
549
- // 8. Install/reconcile system cron entry
519
+ // 7. Install/reconcile system cron entry
550
520
  // ---------------------------------------------------------------------------
551
521
 
552
522
  const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");
@@ -513,6 +513,11 @@ textarea:focus {
513
513
  height: 28px;
514
514
  }
515
515
 
516
+ .ac-segmented-control-full {
517
+ display: flex;
518
+ width: 100%;
519
+ }
520
+
516
521
  .ac-segmented-control-button {
517
522
  border: 0;
518
523
  background: transparent;
@@ -527,6 +532,20 @@ textarea:focus {
527
532
  transition: color 0.12s, background 0.12s;
528
533
  }
529
534
 
535
+ .ac-segmented-control-full .ac-segmented-control-button {
536
+ flex: 1 1 0%;
537
+ }
538
+
539
+ .ac-segmented-control-lg {
540
+ height: 36px;
541
+ border-radius: 12px;
542
+ }
543
+
544
+ .ac-segmented-control-lg .ac-segmented-control-button {
545
+ font-size: 14px;
546
+ padding: 0 16px;
547
+ }
548
+
530
549
  .ac-segmented-control-button:hover {
531
550
  color: var(--text);
532
551
  background: rgba(255, 255, 255, 0.03);
@@ -3,7 +3,7 @@ import { useState, useEffect } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import { Router, Route, Switch, useLocation } from "https://esm.sh/wouter-preact";
5
5
  import { logout } from "./lib/api.js";
6
- import { Welcome } from "./components/welcome.js";
6
+ import { Welcome } from "./components/welcome/index.js";
7
7
  import { ToastContainer } from "./components/toast.js";
8
8
  import { GlobalRestartBanner } from "./components/global-restart-banner.js";
9
9
  import { LoadingSpinner } from "./components/loading-spinner.js";
@@ -1,12 +1,16 @@
1
1
  export const getDoctorPriorityTone = (priority = "") => {
2
- const normalized = String(priority || "").trim().toUpperCase();
2
+ const normalized = String(priority || "")
3
+ .trim()
4
+ .toUpperCase();
3
5
  if (normalized === "P0") return "danger";
4
6
  if (normalized === "P1") return "warning";
5
7
  return "neutral";
6
8
  };
7
9
 
8
10
  export const getDoctorStatusTone = (status = "") => {
9
- const normalized = String(status || "").trim().toLowerCase();
11
+ const normalized = String(status || "")
12
+ .trim()
13
+ .toLowerCase();
10
14
  if (normalized === "fixed") return "success";
11
15
  if (normalized === "dismissed") return "neutral";
12
16
  return "warning";
@@ -35,7 +39,9 @@ export const formatDoctorCategory = (category = "") => {
35
39
  export const buildDoctorPriorityCounts = (cards = []) =>
36
40
  cards.reduce(
37
41
  (totals, card) => {
38
- const priority = String(card?.priority || "").trim().toUpperCase();
42
+ const priority = String(card?.priority || "")
43
+ .trim()
44
+ .toUpperCase();
39
45
  if (priority === "P0" || priority === "P1" || priority === "P2") {
40
46
  totals[priority] += 1;
41
47
  }
@@ -47,7 +53,9 @@ export const buildDoctorPriorityCounts = (cards = []) =>
47
53
  export const groupDoctorCardsByStatus = (cards = []) =>
48
54
  cards.reduce(
49
55
  (groups, card) => {
50
- const status = String(card?.status || "open").trim().toLowerCase();
56
+ const status = String(card?.status || "open")
57
+ .trim()
58
+ .toLowerCase();
51
59
  if (status === "fixed") {
52
60
  groups.fixed.push(card);
53
61
  return groups;
@@ -74,13 +82,71 @@ export const shouldShowDoctorWarning = (
74
82
 
75
83
  export const getDoctorWarningMessage = (doctorStatus = null) => {
76
84
  if (!doctorStatus) return "";
77
- const changedFilesCount = Number(doctorStatus.changeSummary?.changedFilesCount || 0);
85
+ const changedFilesCount = Number(
86
+ doctorStatus.changeSummary?.changedFilesCount || 0,
87
+ );
78
88
  if (changedFilesCount > 0) {
79
89
  return `Drift Doctor has not been run in the last week and ${changedFilesCount} file${changedFilesCount === 1 ? "" : "s"} changed since the last review.`;
80
90
  }
81
91
  return "Doctor has not been run in the last week.";
82
92
  };
83
93
 
94
+ export const formatDoctorCharCount = (value = 0) =>
95
+ `${Number(value || 0).toLocaleString()} chars`;
96
+
97
+ const isManagedBootstrapContextPath = (filePath = "") =>
98
+ String(filePath || "").startsWith("hooks/bootstrap/");
99
+
100
+ export const getDoctorBootstrapTruncationItems = (doctorStatus = null) => {
101
+ const bootstrapContext = doctorStatus?.bootstrapContext;
102
+ const truncatedFiles = (bootstrapContext?.activeTruncatedFiles || []).filter(
103
+ (file) => !isManagedBootstrapContextPath(file?.path),
104
+ );
105
+ const nearLimitFiles = (bootstrapContext?.activeNearLimitFiles || []).filter(
106
+ (file) => !isManagedBootstrapContextPath(file?.path),
107
+ );
108
+ return [
109
+ ...truncatedFiles.map((file) => ({
110
+ path: file.path,
111
+ size: formatDoctorCharCount(file.rawChars),
112
+ statusText: `-${Number(
113
+ Math.max(
114
+ 0,
115
+ Number(file.rawChars || 0) - Number(file.injectedChars || 0),
116
+ ),
117
+ ).toLocaleString()} cut`,
118
+ statusTone: "danger",
119
+ })),
120
+ ...nearLimitFiles.map((file) => ({
121
+ path: file.path,
122
+ size: formatDoctorCharCount(file.rawChars),
123
+ statusText: "Near limit",
124
+ statusTone: "warning",
125
+ })),
126
+ ];
127
+ };
128
+
129
+ export const hasDoctorBootstrapWarnings = (doctorStatus = null) =>
130
+ getDoctorBootstrapTruncationItems(doctorStatus).length > 0;
131
+
132
+ export const getDoctorBootstrapWarningTitle = (doctorStatus = null) => {
133
+ const items = getDoctorBootstrapTruncationItems(doctorStatus);
134
+ if (!items.length) return "";
135
+ const hasTruncatedItems = items.some((item) => item.statusTone === "danger");
136
+ const hasNearLimitItems = items.some((item) => item.statusTone === "warning");
137
+ if (hasTruncatedItems && hasNearLimitItems) {
138
+ return "Some of your main files are being truncated or nearing the limit:";
139
+ }
140
+ if (hasNearLimitItems) {
141
+ return items.length === 1
142
+ ? "One of your main files is nearing the limit:"
143
+ : "Some of your main files are nearing the limit:";
144
+ }
145
+ return items.length === 1
146
+ ? "One of your main files is being truncated:"
147
+ : "Some of your main files are being truncated:";
148
+ };
149
+
84
150
  export const getDoctorChangeLabel = (changeSummary = null) => {
85
151
  const changedFilesCount = Number(changeSummary?.changedFilesCount || 0);
86
152
  if (changedFilesCount === 0) return "No changes since last run";
@@ -20,8 +20,11 @@ import { DoctorFixCardModal } from "./fix-card-modal.js";
20
20
  import {
21
21
  buildDoctorRunMarkers,
22
22
  buildDoctorStatusFilterOptions,
23
+ getDoctorBootstrapTruncationItems,
24
+ getDoctorBootstrapWarningTitle,
23
25
  getDoctorChangeLabel,
24
26
  getDoctorRunPillDetail,
27
+ hasDoctorBootstrapWarnings,
25
28
  shouldShowDoctorWarning,
26
29
  } from "./helpers.js";
27
30
 
@@ -182,6 +185,18 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
182
185
  () => shouldShowDoctorWarning(doctorStatus, 0),
183
186
  [doctorStatus],
184
187
  );
188
+ const showBootstrapTruncationBanner = useMemo(
189
+ () => hasDoctorBootstrapWarnings(doctorStatus),
190
+ [doctorStatus],
191
+ );
192
+ const bootstrapTruncationMessage = useMemo(
193
+ () => getDoctorBootstrapWarningTitle(doctorStatus),
194
+ [doctorStatus],
195
+ );
196
+ const bootstrapTruncationItems = useMemo(
197
+ () => getDoctorBootstrapTruncationItems(doctorStatus),
198
+ [doctorStatus],
199
+ );
185
200
  const hasCompletedDoctorRun = !!doctorStatus?.lastRunAt;
186
201
  const hasRuns = runs.length > 0;
187
202
  const hasLoadedRuns = runsPoll.data !== null || runsPoll.error !== null;
@@ -312,29 +327,76 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
312
327
  : null}
313
328
  ${!showInitialLoadingState && hasRuns
314
329
  ? html`
315
- <${DoctorSummaryCards} cards=${openCards} />
316
- <div class="space-y-2">
317
- ${hasCompletedDoctorRun
318
- ? html`
319
- <div
320
- class="bg-surface border border-border rounded-xl p-4 flex flex-wrap items-center justify-between gap-3"
321
- >
322
- <span class="text-xs text-gray-500">
323
- Last run ·${" "}
324
- <span class="text-gray-300">
325
- ${formatLocaleDateTime(doctorStatus?.lastRunAt, {
326
- fallback: "Never",
327
- })}
330
+ <div class="space-y-3">
331
+ <${DoctorSummaryCards} cards=${openCards} />
332
+ <div class="space-y-3">
333
+ ${hasCompletedDoctorRun
334
+ ? html`
335
+ <div
336
+ class="bg-surface border border-border rounded-xl p-4 flex flex-wrap items-center justify-between gap-3"
337
+ >
338
+ <span class="text-xs text-gray-500">
339
+ Last run ·${" "}
340
+ <span class="text-gray-300">
341
+ ${formatLocaleDateTime(doctorStatus?.lastRunAt, {
342
+ fallback: "Never",
343
+ })}
344
+ </span>
328
345
  </span>
329
- </span>
330
- <span class="text-xs text-gray-500">
331
- ${changeLabel}
332
- </span>
333
- </div>
334
- `
335
- : null}
336
- ${
337
- showDoctorStaleBanner
346
+ <span class="text-xs text-gray-500">
347
+ ${changeLabel}
348
+ </span>
349
+ </div>
350
+ ${showBootstrapTruncationBanner
351
+ ? html`
352
+ <div
353
+ class="bg-surface border border-border rounded-xl p-4 space-y-3"
354
+ >
355
+ <div class="text-xs text-gray-400">
356
+ ⚠️ ${bootstrapTruncationMessage}
357
+ </div>
358
+ <div class="space-y-2">
359
+ ${bootstrapTruncationItems.map(
360
+ (item) => html`
361
+ <div
362
+ class="flex items-center justify-between gap-3 text-xs"
363
+ >
364
+ <button
365
+ type="button"
366
+ class="font-mono text-gray-200 ac-tip-link hover:underline text-left cursor-pointer"
367
+ onClick=${() => onOpenFile(String(item.path || ""))}
368
+ >
369
+ ${item.path}
370
+ </button>
371
+ <span
372
+ class="flex items-center gap-3 whitespace-nowrap"
373
+ >
374
+ <span class="text-gray-500">
375
+ ${item.size}
376
+ </span>
377
+ <span
378
+ class=${item.statusTone === "warning"
379
+ ? "text-yellow-300"
380
+ : "text-red-300"}
381
+ >
382
+ ${item.statusText}
383
+ </span>
384
+ </span>
385
+ </div>
386
+ `,
387
+ )}
388
+ </div>
389
+ <div class="border-t border-border"></div>
390
+ <p class="text-xs text-gray-500 leading-5">
391
+ Truncated files become partially hidden from
392
+ your agent and could cause drift.
393
+ </p>
394
+ </div>
395
+ `
396
+ : null}
397
+ `
398
+ : null}
399
+ ${showDoctorStaleBanner
338
400
  ? html`
339
401
  <div
340
402
  class="text-xs text-yellow-300 bg-yellow-500/10 border border-yellow-500/35 rounded-lg px-3 py-2"
@@ -344,8 +406,8 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
344
406
  changed.
345
407
  </div>
346
408
  `
347
- : null
348
- }
409
+ : null}
410
+ </div>
349
411
  </div>
350
412
  `
351
413
  : null}
@@ -461,9 +523,7 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
461
523
  ${selectedRunIsInProgress
462
524
  ? html`
463
525
  <div class="ac-surface-inset rounded-xl p-4">
464
- <div
465
- class="text-xs leading-5 text-gray-400"
466
- >
526
+ <div class="text-xs leading-5 text-gray-400">
467
527
  <span
468
528
  >Run in progress. Findings will appear when analysis
469
529
  completes.</span
@@ -479,7 +539,8 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
479
539
  onAskAgentFix=${setFixCard}
480
540
  onUpdateStatus=${handleUpdateStatus}
481
541
  onOpenFile=${onOpenFile}
482
- changedPaths=${doctorStatus?.changeSummary?.changedPaths || []}
542
+ changedPaths=${doctorStatus?.changeSummary?.changedPaths ||
543
+ []}
483
544
  showRunMeta=${selectedRunFilter === "all"}
484
545
  hideEmptyState=${selectedRunIsInProgress}
485
546
  />
@@ -86,7 +86,6 @@ const kHintByKey = {
86
86
  OPENAI_API_KEY: html`from <a href="https://platform.openai.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">platform.openai.com</a>`,
87
87
  GEMINI_API_KEY: html`from <a href="https://aistudio.google.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">aistudio.google.com</a>`,
88
88
  ELEVENLABS_API_KEY: html`from <a href="https://elevenlabs.io" target="_blank" class="hover:underline" style="color: var(--accent-link)">elevenlabs.io</a> · <code class="text-xs bg-black/30 px-1 rounded">XI_API_KEY</code> also supported`,
89
- GITHUB_TOKEN: html`classic PAT · <code class="text-xs bg-black/30 px-1 rounded">repo</code> scope · <a href="https://github.com/settings/tokens" target="_blank" class="hover:underline" style="color: var(--accent-link)">github settings</a>`,
90
89
  GITHUB_WORKSPACE_REPO: html`use <code class="text-xs bg-black/30 px-1 rounded">owner/repo</code> or <code class="text-xs bg-black/30 px-1 rounded">https://github.com/owner/repo</code>`,
91
90
  TELEGRAM_BOT_TOKEN: html`from <a href="https://t.me/BotFather" target="_blank" class="hover:underline" style="color: var(--accent-link)">@BotFather</a> · <a href="https://docs.openclaw.ai/channels/telegram" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
92
91
  DISCORD_BOT_TOKEN: html`from <a href="https://discord.com/developers/applications" target="_blank" class="hover:underline" style="color: var(--accent-link)">developer portal</a> · <a href="https://docs.openclaw.ai/channels/discord" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
@@ -4,6 +4,13 @@ import { kAllAiAuthFields } from "../../lib/model-config.js";
4
4
 
5
5
  const html = htm.bind(h);
6
6
 
7
+ export const kRepoModeNew = "new";
8
+ export const kRepoModeExisting = "existing";
9
+ export const kGithubFlowFresh = "fresh";
10
+ export const kGithubFlowImport = "import";
11
+ export const kGithubTargetRepoModeCreate = "create";
12
+ export const kGithubTargetRepoModeExistingEmpty = "existing-empty";
13
+
7
14
  export const normalizeGithubRepoInput = (repoInput) =>
8
15
  String(repoInput || "")
9
16
  .trim()
@@ -19,44 +26,59 @@ export const isValidGithubRepoInput = (repoInput) => {
19
26
  };
20
27
 
21
28
  export const kWelcomeGroups = [
22
- {
23
- id: "ai",
24
- title: "Primary Agent Model",
25
- description: "Choose your main model and authenticate its provider",
26
- fields: kAllAiAuthFields,
27
- validate: (vals, ctx = {}) => !!(vals.MODEL_KEY && ctx.hasAi),
28
- },
29
29
  {
30
30
  id: "github",
31
31
  title: "GitHub",
32
32
  description: "Backs up your agent's config and workspace",
33
33
  fields: [
34
+ {
35
+ key: "_GITHUB_SOURCE_REPO",
36
+ label: "Source Repo",
37
+ placeholder: "username/existing-openclaw",
38
+ isText: true,
39
+ },
34
40
  {
35
41
  key: "GITHUB_WORKSPACE_REPO",
36
- label: "Workspace Repo",
37
- hint: "A new private repo will be created for you",
42
+ label: "New Workspace Repo",
38
43
  placeholder: "username/my-agent",
39
44
  isText: true,
40
45
  },
41
46
  {
42
47
  key: "GITHUB_TOKEN",
43
48
  label: "Personal Access Token",
44
- hint: html`Create a classic PAT on${" "}<a
49
+ hint: html`Create a${" "}<a
45
50
  href="https://github.com/settings/tokens"
46
51
  target="_blank"
47
52
  class="hover:underline"
48
53
  style="color: var(--accent-link)"
49
- >GitHub settings</a
54
+ >classic PAT</a
50
55
  >${" "}with${" "}<code class="text-xs bg-black/30 px-1 rounded"
51
56
  >repo</code
52
- >${" "}scope`,
53
- placeholder: "ghp_...",
57
+ >${" "}scope, or a${" "}<a
58
+ href="https://github.com/settings/personal-access-tokens/new"
59
+ target="_blank"
60
+ class="hover:underline"
61
+ style="color: var(--accent-link)"
62
+ >fine-grained token</a
63
+ >${" "}with Contents + Metadata access`,
64
+ placeholder: "ghp_... or github_pat_...",
54
65
  },
55
66
  ],
56
- validate: (vals) =>
57
- !!(
58
- vals.GITHUB_TOKEN && isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO)
59
- ),
67
+ validate: (vals) => {
68
+ const githubFlow = vals._GITHUB_FLOW || kGithubFlowFresh;
69
+ const hasTarget = isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO);
70
+ const hasSource =
71
+ githubFlow !== kGithubFlowImport ||
72
+ isValidGithubRepoInput(vals._GITHUB_SOURCE_REPO);
73
+ return !!(vals.GITHUB_TOKEN && hasTarget && hasSource);
74
+ },
75
+ },
76
+ {
77
+ id: "ai",
78
+ title: "Primary Agent Model",
79
+ description: "Choose your main model and authenticate its provider",
80
+ fields: kAllAiAuthFields,
81
+ validate: (vals, ctx = {}) => !!(vals.MODEL_KEY && ctx.hasAi),
60
82
  },
61
83
  {
62
84
  id: "channels",