@chrysb/alphaclaw 0.9.0 → 0.9.1

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.
@@ -18,6 +18,7 @@ import {
18
18
  getInitialOnboardingModelKey,
19
19
  getModelCatalogModels,
20
20
  kModelCatalogCacheKey,
21
+ preloadModelCatalog,
21
22
  } from "../../lib/model-catalog.js";
22
23
  import {
23
24
  kWelcomeGroups,
@@ -129,6 +130,11 @@ export const useWelcome = ({ onComplete }) => {
129
130
  maxAgeMs: 30000,
130
131
  });
131
132
 
133
+ useEffect(() => {
134
+ // Warm the real catalog immediately so the AI step usually opens ready.
135
+ preloadModelCatalog().catch(() => {});
136
+ }, []);
137
+
132
138
  const setValue = (key, value) => {
133
139
  if (formError) setFormError(null);
134
140
  setStoredValue(key, value);
@@ -1,3 +1,5 @@
1
+ import { fetchModels } from "./api.js";
2
+ import { cachedFetch } from "./api-cache.js";
1
3
  import { getFeaturedModels } from "./model-config.js";
2
4
 
3
5
  export const kModelCatalogCacheKey = "/api/models";
@@ -9,6 +11,15 @@ export const getModelCatalogModels = (payload) =>
9
11
  export const isModelCatalogRefreshing = (payload) =>
10
12
  Boolean(payload?.refreshing);
11
13
 
14
+ export const preloadModelCatalog = ({
15
+ force = true,
16
+ maxAgeMs = 30000,
17
+ } = {}) =>
18
+ cachedFetch(kModelCatalogCacheKey, fetchModels, {
19
+ force,
20
+ maxAgeMs,
21
+ });
22
+
12
23
  export const getInitialOnboardingModelKey = ({
13
24
  catalog = [],
14
25
  currentModelKey = "",
package/lib/scripts/git CHANGED
@@ -6,21 +6,6 @@ REAL_GIT_HINT="@@REAL_GIT@@"
6
6
  OPENCLAW_REPO_ROOT="@@OPENCLAW_REPO_ROOT@@"
7
7
  ASKPASS_PATH="/tmp/alphaclaw-git-askpass.sh"
8
8
 
9
- SUBCMD=""
10
- for arg in "$@"; do
11
- case "$arg" in
12
- -*) ;;
13
- *) SUBCMD="$arg"; break ;;
14
- esac
15
- done
16
-
17
- needs_auth() {
18
- case "$SUBCMD" in
19
- push|pull|fetch|clone|ls-remote) return 0 ;;
20
- *) return 1 ;;
21
- esac
22
- }
23
-
24
9
  same_path() {
25
10
  local left_path right_path
26
11
  left_path="$(readlink -f "$1" 2>/dev/null || printf '%s' "$1")"
@@ -61,12 +46,73 @@ EOF
61
46
  return 1
62
47
  }
63
48
 
49
+ canonicalize_path() {
50
+ local target_path resolved_path
51
+ target_path="$1"
52
+ [ -n "$target_path" ] || return 1
53
+
54
+ if [ -d "$target_path" ]; then
55
+ resolved_path="$(cd "$target_path" 2>/dev/null && pwd -P)" || resolved_path=""
56
+ if [ -n "$resolved_path" ]; then
57
+ printf '%s\n' "$resolved_path"
58
+ return 0
59
+ fi
60
+ fi
61
+
62
+ resolved_path="$(readlink -f "$target_path" 2>/dev/null || true)"
63
+ if [ -n "$resolved_path" ]; then
64
+ printf '%s\n' "$resolved_path"
65
+ return 0
66
+ fi
67
+
68
+ printf '%s\n' "$target_path"
69
+ }
70
+
71
+ resolve_effective_pwd() {
72
+ local effective_pwd
73
+ effective_pwd="$(pwd)"
74
+
75
+ while [ "$#" -gt 0 ]; do
76
+ case "$1" in
77
+ -C)
78
+ shift
79
+ [ "$#" -gt 0 ] || break
80
+ case "$1" in
81
+ /*) effective_pwd="$1" ;;
82
+ *) effective_pwd="$effective_pwd/$1" ;;
83
+ esac
84
+ ;;
85
+ -c|--exec-path|--git-dir|--work-tree|--namespace|--config-env|--super-prefix|--list-cmds|--attr-source)
86
+ shift
87
+ [ "$#" -gt 0 ] || break
88
+ ;;
89
+ --exec-path=*|--git-dir=*|--work-tree=*|--namespace=*|--config-env=*|--super-prefix=*|--list-cmds=*|--attr-source=*)
90
+ ;;
91
+ --)
92
+ break
93
+ ;;
94
+ -*)
95
+ ;;
96
+ *)
97
+ break
98
+ ;;
99
+ esac
100
+ shift
101
+ done
102
+
103
+ canonicalize_path "$effective_pwd"
104
+ }
105
+
64
106
  in_openclaw_root() {
107
+ local candidate_path resolved_repo_root resolved_candidate_path
108
+ candidate_path="$1"
65
109
  if [ -z "$OPENCLAW_REPO_ROOT" ]; then
66
110
  return 1
67
111
  fi
68
- case "$(pwd)" in
69
- "$OPENCLAW_REPO_ROOT"|"${OPENCLAW_REPO_ROOT}"/*) return 0 ;;
112
+ resolved_repo_root="$(canonicalize_path "$OPENCLAW_REPO_ROOT")"
113
+ resolved_candidate_path="$(canonicalize_path "$candidate_path")"
114
+ case "$resolved_candidate_path" in
115
+ "$resolved_repo_root"|"${resolved_repo_root}"/*) return 0 ;;
70
116
  *) return 1 ;;
71
117
  esac
72
118
  }
@@ -77,7 +123,9 @@ if [ -z "$REAL_GIT" ]; then
77
123
  exit 127
78
124
  fi
79
125
 
80
- if [ "${ALPHACLAW_GIT_NO_AUTH:-}" = "1" ] || [ -z "${GITHUB_TOKEN:-}" ] || ! needs_auth || ! in_openclaw_root; then
126
+ EFFECTIVE_PWD="$(resolve_effective_pwd "$@")"
127
+
128
+ if [ "${ALPHACLAW_GIT_NO_AUTH:-}" = "1" ] || [ -z "${GITHUB_TOKEN:-}" ] || ! in_openclaw_root "$EFFECTIVE_PWD"; then
81
129
  exec "$REAL_GIT" "$@"
82
130
  fi
83
131
 
@@ -45,6 +45,7 @@ const gatewayEnv = () => ({
45
45
  ...process.env,
46
46
  OPENCLAW_HOME: kRootDir,
47
47
  OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
48
+ OPENCLAW_STATE_DIR: OPENCLAW_DIR,
48
49
  XDG_CONFIG_HOME: OPENCLAW_DIR,
49
50
  });
50
51
 
@@ -123,7 +123,7 @@ const createModelCatalogCache = ({
123
123
  const loadFreshCatalog = async () => {
124
124
  const output = await shellCmd("openclaw models list --all --json", {
125
125
  env: gatewayEnv(),
126
- timeout: 20000,
126
+ timeout: 30000,
127
127
  });
128
128
  const parsed = parseJsonFromNoisyOutput(output);
129
129
  const models = normalizeOnboardingModels(parsed?.models || []);
@@ -63,6 +63,56 @@ const repoContainsOnlyBoilerplate = async (repoUrl, ghHeaders) => {
63
63
  }
64
64
  };
65
65
 
66
+ const getNextGithubPageUrl = (linkHeader = "") => {
67
+ const nextLink = String(linkHeader || "")
68
+ .split(",")
69
+ .map((entry) => entry.trim())
70
+ .find((entry) => entry.endsWith('rel="next"'));
71
+ const match = nextLink?.match(/<([^>]+)>/);
72
+ return match?.[1] || "";
73
+ };
74
+
75
+ const findOwnedRepoByName = async ({
76
+ repoUrl,
77
+ repoOwner,
78
+ repoName,
79
+ viewerLogin,
80
+ ghHeaders,
81
+ }) => {
82
+ if (
83
+ !repoOwner ||
84
+ !repoName ||
85
+ !viewerLogin ||
86
+ repoOwner.toLowerCase() !== viewerLogin.toLowerCase()
87
+ ) {
88
+ return null;
89
+ }
90
+
91
+ let nextUrl =
92
+ "https://api.github.com/user/repos?affiliation=owner&per_page=100&page=1";
93
+ const normalizedRepoUrl = String(repoUrl || "").trim().toLowerCase();
94
+ const normalizedRepoName = String(repoName || "").trim().toLowerCase();
95
+
96
+ while (nextUrl) {
97
+ const res = await fetch(nextUrl, { headers: ghHeaders });
98
+ if (!res.ok) return null;
99
+
100
+ const repos = await res.json();
101
+ if (!Array.isArray(repos)) return null;
102
+
103
+ const existingRepo = repos.find((repo) => {
104
+ const fullName = String(repo?.full_name || "").trim().toLowerCase();
105
+ const name = String(repo?.name || "").trim().toLowerCase();
106
+ return fullName === normalizedRepoUrl || name === normalizedRepoName;
107
+ });
108
+ if (existingRepo) return existingRepo;
109
+
110
+ nextUrl = getNextGithubPageUrl(res.headers?.get?.("link"));
111
+ }
112
+
113
+ return null;
114
+ };
115
+
66
116
  const isClassicPat = (token) => String(token || "").startsWith("ghp_");
67
117
  const isFineGrainedPat = (token) =>
68
118
  String(token || "").startsWith("github_pat_");
@@ -73,8 +123,9 @@ const verifyGithubRepoForOnboarding = async ({
73
123
  mode = "new",
74
124
  }) => {
75
125
  const ghHeaders = buildGithubHeaders(githubToken);
76
- const [repoOwner] = String(repoUrl || "").split("/", 1);
126
+ const [repoOwner = "", repoName = ""] = String(repoUrl || "").split("/");
77
127
  const isExisting = mode === "existing";
128
+ let viewerLogin = "";
78
129
 
79
130
  try {
80
131
  const userRes = await fetch("https://api.github.com/user", {
@@ -106,12 +157,29 @@ const verifyGithubRepoForOnboarding = async ({
106
157
  };
107
158
  }
108
159
  }
109
- await userRes.json().catch(() => ({}));
160
+ const userPayload = await userRes.json().catch(() => ({}));
161
+ viewerLogin = String(userPayload?.login || "").trim();
110
162
 
111
163
  const checkRes = await fetch(`https://api.github.com/repos/${repoUrl}`, {
112
164
  headers: ghHeaders,
113
165
  });
114
166
  if (checkRes.status === 404) {
167
+ const hiddenOwnedRepo = await findOwnedRepoByName({
168
+ repoUrl,
169
+ repoOwner,
170
+ repoName,
171
+ viewerLogin,
172
+ ghHeaders,
173
+ });
174
+ if (hiddenOwnedRepo) {
175
+ return {
176
+ ok: false,
177
+ status: 400,
178
+ error:
179
+ `Repository "${repoUrl}" already exists, but this token cannot inspect it. ` +
180
+ "Choose a different repo name or use a token that can access that repo.",
181
+ };
182
+ }
115
183
  if (isExisting) {
116
184
  return {
117
185
  ok: false,
@@ -205,6 +273,19 @@ const ensureGithubRepoAccessible = async ({
205
273
  });
206
274
  if (!createRes.ok) {
207
275
  const details = await parseGithubErrorMessage(createRes);
276
+ if (
277
+ String(details || "")
278
+ .toLowerCase()
279
+ .includes("name already exists on this account")
280
+ ) {
281
+ return {
282
+ ok: false,
283
+ status: 400,
284
+ error:
285
+ `Repository "${repoUrl}" already exists. ` +
286
+ "Choose a different repo name or use a token that can access that repo.",
287
+ };
288
+ }
208
289
  const hint =
209
290
  createRes.status === 404 || createRes.status === 403
210
291
  ? ' Ensure your token is a classic PAT with the "repo" scope.'
@@ -495,6 +495,8 @@ const createOnboardingService = ({
495
495
  ...process.env,
496
496
  OPENCLAW_HOME: kRootDir,
497
497
  OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
498
+ OPENCLAW_STATE_DIR: OPENCLAW_DIR,
499
+ XDG_CONFIG_HOME: OPENCLAW_DIR,
498
500
  },
499
501
  timeout: 120000,
500
502
  },
@@ -25,6 +25,18 @@ If any of these apply, outline your approach first — what you intend to do, in
25
25
 
26
26
  Your `.openclaw` directory is version-controlled and this is how work survives container restarts.
27
27
 
28
+ ### Persistent Storage Rules
29
+
30
+ This deployment runs in an ephemeral container. `/tmp`, other temp directories, and files outside `/data` can disappear on restart or redeploy.
31
+
32
+ Anything that must survive redeploys must live under `/data/.openclaw`.
33
+
34
+ For plugins and other durable artifacts:
35
+
36
+ - Prefer normal `openclaw plugins install <spec>` flows for persistent installs.
37
+ - If you must stage or unpack a local plugin first, stage it under `/data/.openclaw/...`, not `/tmp/...`.
38
+ - Never persist `plugins.load.paths` entries that point at temp directories.
39
+
28
40
  Anytime you add, edit, or remove workspace files, openclaw.json, cron.json, skills, or external resources (third-party pages, databases, integrations), **commit and push your changes to git**. Never force push; always pull first if there might be remote changes.
29
41
 
30
42
  Whenever you do this, end your message with a **Changes committed** summary. Use workspace-relative paths for local files.
@@ -21,6 +21,18 @@ Do not deflect actionable requests to the Setup UI. If a command or tool is avai
21
21
 
22
22
  Changes to env vars are made through the **Envars** tab (`{{SETUP_UI_URL}}#envars`). After saving, a gateway restart may be required to pick up the changes — the UI prompts for this automatically. Do not edit `/data/.env` directly; use the Setup UI so changes are validated and the gateway restart is handled.
23
23
 
24
+ ### Persistent storage
25
+
26
+ This deployment runs in an ephemeral container. `/tmp` and other temp locations do not survive redeploys.
27
+
28
+ Anything persistent must live under `/data/.openclaw`.
29
+
30
+ For plugins and local tooling:
31
+
32
+ - Prefer normal `openclaw plugins install <spec>` flows for durable installs.
33
+ - If you need to stage a local plugin or helper files first, put them under `/data/.openclaw/...`, not `/tmp/...`.
34
+ - Do not leave durable `plugins.load.paths` entries pointing at temp directories.
35
+
24
36
  ### Google Workspace
25
37
 
26
38
  Google Workspace is connected via the **General** tab (`{{SETUP_UI_URL}}#general`). The user provides OAuth client credentials from Google Cloud Console, then authorizes access to the services they need (Gmail, Calendar, Drive, Sheets, Docs, Tasks, Contacts, Meet). Connected accounts and `gog` CLI usage are covered by the gog-cli skill.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "express": "^4.21.0",
38
38
  "http-proxy": "^1.18.1",
39
- "openclaw": "2026.4.9",
39
+ "openclaw": "2026.4.10",
40
40
  "patch-package": "^8.0.1",
41
41
  "ws": "^8.19.0"
42
42
  },