@chrysb/alphaclaw 0.1.0
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 +338 -0
- package/lib/public/icons/chevron-down.svg +9 -0
- package/lib/public/js/app.js +325 -0
- package/lib/public/js/components/badge.js +16 -0
- package/lib/public/js/components/channels.js +36 -0
- package/lib/public/js/components/credentials-modal.js +336 -0
- package/lib/public/js/components/device-pairings.js +72 -0
- package/lib/public/js/components/envars.js +354 -0
- package/lib/public/js/components/gateway.js +163 -0
- package/lib/public/js/components/google.js +223 -0
- package/lib/public/js/components/icons.js +23 -0
- package/lib/public/js/components/models.js +461 -0
- package/lib/public/js/components/pairings.js +74 -0
- package/lib/public/js/components/scope-picker.js +106 -0
- package/lib/public/js/components/toast.js +31 -0
- package/lib/public/js/components/welcome.js +541 -0
- package/lib/public/js/hooks/usePolling.js +29 -0
- package/lib/public/js/lib/api.js +196 -0
- package/lib/public/js/lib/model-config.js +88 -0
- package/lib/public/login.html +90 -0
- package/lib/public/setup.html +33 -0
- package/lib/scripts/systemctl +56 -0
- package/lib/server/auth-profiles.js +101 -0
- package/lib/server/commands.js +84 -0
- package/lib/server/constants.js +282 -0
- package/lib/server/env.js +78 -0
- package/lib/server/gateway.js +262 -0
- package/lib/server/helpers.js +192 -0
- package/lib/server/login-throttle.js +86 -0
- package/lib/server/onboarding/cron.js +51 -0
- package/lib/server/onboarding/github.js +49 -0
- package/lib/server/onboarding/index.js +127 -0
- package/lib/server/onboarding/openclaw.js +171 -0
- package/lib/server/onboarding/validation.js +107 -0
- package/lib/server/onboarding/workspace.js +52 -0
- package/lib/server/openclaw-version.js +179 -0
- package/lib/server/routes/auth.js +80 -0
- package/lib/server/routes/codex.js +204 -0
- package/lib/server/routes/google.js +390 -0
- package/lib/server/routes/models.js +68 -0
- package/lib/server/routes/onboarding.js +116 -0
- package/lib/server/routes/pages.js +21 -0
- package/lib/server/routes/pairings.js +134 -0
- package/lib/server/routes/proxy.js +29 -0
- package/lib/server/routes/system.js +213 -0
- package/lib/server.js +161 -0
- package/lib/setup/core-prompts/AGENTS.md +22 -0
- package/lib/setup/core-prompts/TOOLS.md +18 -0
- package/lib/setup/env.template +19 -0
- package/lib/setup/gitignore +12 -0
- package/lib/setup/hourly-git-sync.sh +86 -0
- package/lib/setup/skills/control-ui/SKILL.md +70 -0
- package/package.json +34 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
const registerGoogleRoutes = ({
|
|
2
|
+
app,
|
|
3
|
+
fs,
|
|
4
|
+
isGatewayRunning,
|
|
5
|
+
gogCmd,
|
|
6
|
+
getBaseUrl,
|
|
7
|
+
readGoogleCredentials,
|
|
8
|
+
getApiEnableUrl,
|
|
9
|
+
constants,
|
|
10
|
+
}) => {
|
|
11
|
+
const {
|
|
12
|
+
GOG_CREDENTIALS_PATH,
|
|
13
|
+
GOG_CONFIG_DIR,
|
|
14
|
+
GOG_STATE_PATH,
|
|
15
|
+
API_TEST_COMMANDS,
|
|
16
|
+
BASE_SCOPES,
|
|
17
|
+
SCOPE_MAP,
|
|
18
|
+
REVERSE_SCOPE_MAP,
|
|
19
|
+
} = constants;
|
|
20
|
+
|
|
21
|
+
app.get("/api/google/status", async (req, res) => {
|
|
22
|
+
if (!(await isGatewayRunning())) {
|
|
23
|
+
return res.json({
|
|
24
|
+
hasCredentials: false,
|
|
25
|
+
authenticated: false,
|
|
26
|
+
email: "",
|
|
27
|
+
services: "",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const hasCredentials = fs.existsSync(GOG_CREDENTIALS_PATH);
|
|
31
|
+
let authenticated = false;
|
|
32
|
+
let email = "";
|
|
33
|
+
|
|
34
|
+
if (hasCredentials) {
|
|
35
|
+
const result = await gogCmd("auth list --plain", { quiet: true });
|
|
36
|
+
if (result.ok && result.stdout && !result.stdout.includes("no accounts")) {
|
|
37
|
+
authenticated = true;
|
|
38
|
+
email = result.stdout.split("\n")[0]?.split("\t")[0] || "";
|
|
39
|
+
}
|
|
40
|
+
if (!email) {
|
|
41
|
+
try {
|
|
42
|
+
const state = JSON.parse(fs.readFileSync(GOG_STATE_PATH, "utf8"));
|
|
43
|
+
email = state.email || "";
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let services = "";
|
|
49
|
+
let activeScopes = [];
|
|
50
|
+
try {
|
|
51
|
+
const stateData = JSON.parse(fs.readFileSync(GOG_STATE_PATH, "utf8"));
|
|
52
|
+
activeScopes = stateData.services || [];
|
|
53
|
+
services = activeScopes
|
|
54
|
+
.map((s) => s.split(":")[0])
|
|
55
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
56
|
+
.join(", ");
|
|
57
|
+
} catch {}
|
|
58
|
+
|
|
59
|
+
const status = {
|
|
60
|
+
hasCredentials,
|
|
61
|
+
authenticated,
|
|
62
|
+
email,
|
|
63
|
+
services,
|
|
64
|
+
activeScopes,
|
|
65
|
+
};
|
|
66
|
+
console.log(`[wrapper] Google status: ${JSON.stringify(status)}`);
|
|
67
|
+
res.json(status);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
app.post("/api/google/credentials", async (req, res) => {
|
|
71
|
+
const { clientId, clientSecret, email } = req.body;
|
|
72
|
+
if (!clientId || !clientSecret || !email) {
|
|
73
|
+
return res.json({ ok: false, error: "Missing fields" });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
fs.mkdirSync(GOG_CONFIG_DIR, { recursive: true });
|
|
78
|
+
const credentials = {
|
|
79
|
+
web: {
|
|
80
|
+
client_id: clientId,
|
|
81
|
+
client_secret: clientSecret,
|
|
82
|
+
auth_uri: "https://accounts.google.com/o/oauth2/auth",
|
|
83
|
+
token_uri: "https://oauth2.googleapis.com/token",
|
|
84
|
+
redirect_uris: [`${getBaseUrl(req)}/auth/google/callback`],
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
fs.writeFileSync(GOG_CREDENTIALS_PATH, JSON.stringify(credentials, null, 2));
|
|
89
|
+
const result = await gogCmd(`auth credentials set ${GOG_CREDENTIALS_PATH}`);
|
|
90
|
+
console.log(`[wrapper] gog credentials set: ${JSON.stringify(result)}`);
|
|
91
|
+
|
|
92
|
+
const services = req.body.services || [
|
|
93
|
+
"gmail:read",
|
|
94
|
+
"gmail:write",
|
|
95
|
+
"calendar:read",
|
|
96
|
+
"calendar:write",
|
|
97
|
+
"drive:read",
|
|
98
|
+
"sheets:read",
|
|
99
|
+
"docs:read",
|
|
100
|
+
];
|
|
101
|
+
fs.writeFileSync(GOG_STATE_PATH, JSON.stringify({ email, services }));
|
|
102
|
+
|
|
103
|
+
res.json({ ok: true });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error("[wrapper] Failed to save Google credentials:", err);
|
|
106
|
+
res.json({ ok: false, error: err.message });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.get("/api/google/check", async (req, res) => {
|
|
111
|
+
let email = "";
|
|
112
|
+
let activeScopes = [];
|
|
113
|
+
try {
|
|
114
|
+
const stateData = JSON.parse(fs.readFileSync(GOG_STATE_PATH, "utf8"));
|
|
115
|
+
email = stateData.email || "";
|
|
116
|
+
activeScopes = stateData.services || [];
|
|
117
|
+
} catch {}
|
|
118
|
+
if (!email) return res.json({ error: "No Google account configured" });
|
|
119
|
+
|
|
120
|
+
const enabledServices = activeScopes
|
|
121
|
+
.map((s) => s.split(":")[0])
|
|
122
|
+
.filter((v, i, a) => a.indexOf(v) === i);
|
|
123
|
+
const results = {};
|
|
124
|
+
|
|
125
|
+
for (const svc of enabledServices) {
|
|
126
|
+
const cmd = API_TEST_COMMANDS[svc];
|
|
127
|
+
if (!cmd) continue;
|
|
128
|
+
|
|
129
|
+
const result = await gogCmd(`${cmd} --account ${email}`, { quiet: true });
|
|
130
|
+
const stderr = result.stderr || "";
|
|
131
|
+
if (stderr.includes("has not been used") || stderr.includes("is not enabled")) {
|
|
132
|
+
const projectMatch = stderr.match(/project=(\d+)/);
|
|
133
|
+
results[svc] = {
|
|
134
|
+
status: "not_enabled",
|
|
135
|
+
enableUrl: getApiEnableUrl(svc, projectMatch?.[1]),
|
|
136
|
+
};
|
|
137
|
+
} else if (result.ok || stderr.includes("not found") || stderr.includes("Not Found")) {
|
|
138
|
+
results[svc] = { status: "ok", enableUrl: getApiEnableUrl(svc) };
|
|
139
|
+
} else {
|
|
140
|
+
console.log(`[wrapper] API check ${svc} error: ${result.stderr?.slice(0, 300)}`);
|
|
141
|
+
results[svc] = {
|
|
142
|
+
status: "error",
|
|
143
|
+
message: result.stderr?.slice(0, 200),
|
|
144
|
+
enableUrl: getApiEnableUrl(svc),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
res.json({ email, results });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
app.post("/api/google/disconnect", async (req, res) => {
|
|
153
|
+
try {
|
|
154
|
+
let email = "";
|
|
155
|
+
try {
|
|
156
|
+
const stateData = JSON.parse(fs.readFileSync(GOG_STATE_PATH, "utf8"));
|
|
157
|
+
email = stateData.email || "";
|
|
158
|
+
} catch {}
|
|
159
|
+
|
|
160
|
+
if (email) {
|
|
161
|
+
const exportResult = await gogCmd(
|
|
162
|
+
`auth tokens export ${email} --out /tmp/gog-revoke.json --overwrite`,
|
|
163
|
+
{ quiet: true },
|
|
164
|
+
);
|
|
165
|
+
if (exportResult.ok) {
|
|
166
|
+
try {
|
|
167
|
+
const tokenData = JSON.parse(fs.readFileSync("/tmp/gog-revoke.json", "utf8"));
|
|
168
|
+
if (tokenData.refresh_token) {
|
|
169
|
+
await fetch(`https://oauth2.googleapis.com/revoke?token=${tokenData.refresh_token}`, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
});
|
|
172
|
+
console.log(`[wrapper] Revoked Google token for ${email}`);
|
|
173
|
+
}
|
|
174
|
+
fs.unlinkSync("/tmp/gog-revoke.json");
|
|
175
|
+
} catch {}
|
|
176
|
+
}
|
|
177
|
+
await gogCmd(`auth remove ${email} --force`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const f of [GOG_STATE_PATH, GOG_CREDENTIALS_PATH]) {
|
|
181
|
+
try {
|
|
182
|
+
fs.unlinkSync(f);
|
|
183
|
+
console.log(`[wrapper] Deleted ${f}`);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
if (e.code !== "ENOENT") {
|
|
186
|
+
console.error(`[wrapper] Failed to delete ${f}: ${e.message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const stateStillExists = fs.existsSync(GOG_STATE_PATH);
|
|
192
|
+
const credsStillExists = fs.existsSync(GOG_CREDENTIALS_PATH);
|
|
193
|
+
if (stateStillExists || credsStillExists) {
|
|
194
|
+
console.error(
|
|
195
|
+
`[wrapper] Files survived deletion! state=${stateStillExists} creds=${credsStillExists}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(`[wrapper] Google disconnected: ${email}`);
|
|
200
|
+
res.json({ ok: true });
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error("[wrapper] Google disconnect error:", err);
|
|
203
|
+
res.json({ ok: false, error: err.message });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
app.get("/auth/google/start", (req, res) => {
|
|
208
|
+
const email = req.query.email || "";
|
|
209
|
+
const services = (
|
|
210
|
+
req.query.services ||
|
|
211
|
+
"gmail:read,gmail:write,calendar:read,calendar:write,drive:read,sheets:read,docs:read"
|
|
212
|
+
)
|
|
213
|
+
.split(",")
|
|
214
|
+
.filter(Boolean);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const { clientId } = readGoogleCredentials();
|
|
218
|
+
if (!clientId) throw new Error("No client_id found");
|
|
219
|
+
|
|
220
|
+
const scopes = [
|
|
221
|
+
...BASE_SCOPES,
|
|
222
|
+
...services.map((s) => SCOPE_MAP[s]).filter(Boolean),
|
|
223
|
+
].join(" ");
|
|
224
|
+
console.log(
|
|
225
|
+
`[wrapper] Google OAuth scopes: services=${services.join(",")} resolved=${scopes}`,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const redirectUri = `${getBaseUrl(req)}/auth/google/callback`;
|
|
229
|
+
const state = Buffer.from(JSON.stringify({ email, services })).toString(
|
|
230
|
+
"base64url",
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const authUrl = new URL("https://accounts.google.com/o/oauth2/auth");
|
|
234
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
235
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
236
|
+
authUrl.searchParams.set("response_type", "code");
|
|
237
|
+
authUrl.searchParams.set("scope", scopes);
|
|
238
|
+
authUrl.searchParams.set("access_type", "offline");
|
|
239
|
+
authUrl.searchParams.set("prompt", "consent");
|
|
240
|
+
authUrl.searchParams.set("state", state);
|
|
241
|
+
if (email) authUrl.searchParams.set("login_hint", email);
|
|
242
|
+
|
|
243
|
+
res.redirect(authUrl.toString());
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.error("[wrapper] Failed to start Google auth:", err);
|
|
246
|
+
res.redirect("/setup?google=error&message=" + encodeURIComponent(err.message));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
app.get("/auth/google/callback", async (req, res) => {
|
|
251
|
+
const { code, error, state } = req.query;
|
|
252
|
+
if (error) return res.redirect("/setup?google=error&message=" + encodeURIComponent(error));
|
|
253
|
+
if (!code) return res.redirect("/setup?google=error&message=no_code");
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
let email = "";
|
|
257
|
+
try {
|
|
258
|
+
const decoded = JSON.parse(Buffer.from(state, "base64url").toString());
|
|
259
|
+
email = decoded.email || "";
|
|
260
|
+
} catch {}
|
|
261
|
+
|
|
262
|
+
const { clientId, clientSecret } = readGoogleCredentials();
|
|
263
|
+
const redirectUri = `${getBaseUrl(req)}/auth/google/callback`;
|
|
264
|
+
|
|
265
|
+
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
268
|
+
body: new URLSearchParams({
|
|
269
|
+
code,
|
|
270
|
+
client_id: clientId,
|
|
271
|
+
client_secret: clientSecret,
|
|
272
|
+
redirect_uri: redirectUri,
|
|
273
|
+
grant_type: "authorization_code",
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const tokens = await tokenRes.json();
|
|
278
|
+
if (!tokenRes.ok || tokens.error) {
|
|
279
|
+
console.log(
|
|
280
|
+
`[wrapper] Google token exchange failed: status=${tokenRes.status} error=${tokens.error} desc=${tokens.error_description}`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (tokens.error) {
|
|
284
|
+
throw new Error(`Google token error: ${tokens.error_description || tokens.error}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!tokens.refresh_token) {
|
|
288
|
+
let hasExisting = false;
|
|
289
|
+
try {
|
|
290
|
+
const stateData = JSON.parse(fs.readFileSync(GOG_STATE_PATH, "utf8"));
|
|
291
|
+
hasExisting = stateData.authenticated;
|
|
292
|
+
} catch {}
|
|
293
|
+
|
|
294
|
+
if (hasExisting) {
|
|
295
|
+
console.log(
|
|
296
|
+
"[wrapper] No new refresh token (already authorized), keeping existing",
|
|
297
|
+
);
|
|
298
|
+
} else {
|
|
299
|
+
throw new Error(
|
|
300
|
+
"No refresh token received. Revoke app access at myaccount.google.com/permissions and retry.",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!email && tokens.access_token) {
|
|
306
|
+
try {
|
|
307
|
+
const infoRes = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
|
|
308
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
|
309
|
+
});
|
|
310
|
+
const info = await infoRes.json();
|
|
311
|
+
email = info.email || email;
|
|
312
|
+
} catch {}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (tokens.refresh_token) {
|
|
316
|
+
const tokenFile = `/tmp/gog-token-${Date.now()}.json`;
|
|
317
|
+
const tokenData = {
|
|
318
|
+
email,
|
|
319
|
+
client: "default",
|
|
320
|
+
created_at: new Date().toISOString(),
|
|
321
|
+
refresh_token: tokens.refresh_token,
|
|
322
|
+
};
|
|
323
|
+
fs.writeFileSync(tokenFile, JSON.stringify(tokenData, null, 2));
|
|
324
|
+
const result = await gogCmd(`auth tokens import ${tokenFile}`);
|
|
325
|
+
if (result.ok) {
|
|
326
|
+
console.log(`[wrapper] Google token imported for ${email}`);
|
|
327
|
+
} else {
|
|
328
|
+
console.error(`[wrapper] Token import failed: ${result.stderr}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!result.ok) {
|
|
332
|
+
console.error("[wrapper] Token import failed, trying gog auth add --manual");
|
|
333
|
+
const keyringDir = `${GOG_CONFIG_DIR}/keyring`;
|
|
334
|
+
fs.mkdirSync(keyringDir, { recursive: true });
|
|
335
|
+
fs.writeFileSync(
|
|
336
|
+
`${keyringDir}/token-${email}.json`,
|
|
337
|
+
JSON.stringify(tokenData, null, 2),
|
|
338
|
+
);
|
|
339
|
+
console.log(
|
|
340
|
+
`[wrapper] Token written directly to keyring: ${keyringDir}/token-${email}.json`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
fs.unlinkSync(tokenFile);
|
|
346
|
+
} catch {}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let services = [];
|
|
350
|
+
try {
|
|
351
|
+
const decoded = JSON.parse(Buffer.from(state, "base64url").toString());
|
|
352
|
+
services = decoded.services || [];
|
|
353
|
+
} catch {}
|
|
354
|
+
|
|
355
|
+
const grantedServices = tokens.scope
|
|
356
|
+
? tokens.scope
|
|
357
|
+
.split(" ")
|
|
358
|
+
.map((s) => REVERSE_SCOPE_MAP[s])
|
|
359
|
+
.filter(Boolean)
|
|
360
|
+
: services;
|
|
361
|
+
console.log(
|
|
362
|
+
`[wrapper] Requested: ${services.join(",")} → Granted: ${grantedServices.join(",")}`,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
fs.writeFileSync(
|
|
366
|
+
GOG_STATE_PATH,
|
|
367
|
+
JSON.stringify({
|
|
368
|
+
email,
|
|
369
|
+
clientId,
|
|
370
|
+
clientSecret,
|
|
371
|
+
services: grantedServices,
|
|
372
|
+
authenticated: true,
|
|
373
|
+
}),
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
res.send(`<!DOCTYPE html><html><body><script>
|
|
377
|
+
window.opener?.postMessage({ google: 'success', email: '${email}' }, '*');
|
|
378
|
+
window.close();
|
|
379
|
+
</script><p>Google connected! You can close this window.</p></body></html>`);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
console.error("[wrapper] Google OAuth callback error:", err);
|
|
382
|
+
res.send(`<!DOCTYPE html><html><body><script>
|
|
383
|
+
window.opener?.postMessage({ google: 'error', message: '${err.message.replace(/'/g, "\\'")}' }, '*');
|
|
384
|
+
window.close();
|
|
385
|
+
</script><p>Error: ${err.message}. You can close this window.</p></body></html>`);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
module.exports = { registerGoogleRoutes };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { kFallbackOnboardingModels } = require("../constants");
|
|
2
|
+
|
|
3
|
+
const registerModelRoutes = ({ app, shellCmd, gatewayEnv, parseJsonFromNoisyOutput, normalizeOnboardingModels }) => {
|
|
4
|
+
app.get("/api/models", async (req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
const output = await shellCmd("openclaw models list --all --json", {
|
|
7
|
+
env: gatewayEnv(),
|
|
8
|
+
timeout: 20000,
|
|
9
|
+
});
|
|
10
|
+
const parsed = parseJsonFromNoisyOutput(output);
|
|
11
|
+
const models = normalizeOnboardingModels(parsed?.models || []);
|
|
12
|
+
if (models.length > 0) {
|
|
13
|
+
return res.json({ ok: true, source: "openclaw", models });
|
|
14
|
+
}
|
|
15
|
+
return res.json({
|
|
16
|
+
ok: true,
|
|
17
|
+
source: "fallback",
|
|
18
|
+
models: kFallbackOnboardingModels,
|
|
19
|
+
});
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error("[models] Failed to load dynamic models:", err.message);
|
|
22
|
+
return res.json({
|
|
23
|
+
ok: true,
|
|
24
|
+
source: "fallback",
|
|
25
|
+
models: kFallbackOnboardingModels,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.get("/api/models/status", async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const output = await shellCmd("openclaw models status --json", {
|
|
33
|
+
env: gatewayEnv(),
|
|
34
|
+
timeout: 20000,
|
|
35
|
+
});
|
|
36
|
+
const parsed = parseJsonFromNoisyOutput(output) || {};
|
|
37
|
+
res.json({
|
|
38
|
+
ok: true,
|
|
39
|
+
modelKey: parsed.resolvedDefault || parsed.defaultModel || null,
|
|
40
|
+
fallbacks: parsed.fallbacks || [],
|
|
41
|
+
imageModel: parsed.imageModel || null,
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
res.json({
|
|
45
|
+
ok: false,
|
|
46
|
+
error: err.message || "Failed to read model status",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
app.post("/api/models/set", async (req, res) => {
|
|
52
|
+
const { modelKey } = req.body || {};
|
|
53
|
+
if (!modelKey || typeof modelKey !== "string" || !modelKey.includes("/")) {
|
|
54
|
+
return res.status(400).json({ ok: false, error: "Missing modelKey" });
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await shellCmd(`openclaw models set "${modelKey}"`, {
|
|
58
|
+
env: gatewayEnv(),
|
|
59
|
+
timeout: 30000,
|
|
60
|
+
});
|
|
61
|
+
res.json({ ok: true });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
res.status(400).json({ ok: false, error: err.message || "Failed to set model" });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
module.exports = { registerModelRoutes };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const { createOnboardingService } = require("../onboarding");
|
|
2
|
+
|
|
3
|
+
const sanitizeOnboardingError = (error) => {
|
|
4
|
+
const raw = [error?.stderr, error?.stdout, error?.message]
|
|
5
|
+
.filter((value) => typeof value === "string" && value.trim())
|
|
6
|
+
.join("\n");
|
|
7
|
+
const redacted = String(raw || "Onboarding failed")
|
|
8
|
+
.replace(/sk-[^\s"]+/g, "***")
|
|
9
|
+
.replace(/ghp_[^\s"]+/g, "***")
|
|
10
|
+
.replace(/(?:token|api[_-]?key)["'\s:=]+[^\s"']+/gi, (match) =>
|
|
11
|
+
match.replace(/[^\s"':=]+$/g, "***"),
|
|
12
|
+
);
|
|
13
|
+
const lower = redacted.toLowerCase();
|
|
14
|
+
if (
|
|
15
|
+
lower.includes("heap out of memory") ||
|
|
16
|
+
lower.includes("allocation failed") ||
|
|
17
|
+
lower.includes("fatal error: ineffective mark-compacts")
|
|
18
|
+
) {
|
|
19
|
+
return "Onboarding ran out of memory. Please retry, and if it persists increase instance memory.";
|
|
20
|
+
}
|
|
21
|
+
if (
|
|
22
|
+
lower.includes("permission denied") ||
|
|
23
|
+
lower.includes("denied to") ||
|
|
24
|
+
lower.includes("permission to") ||
|
|
25
|
+
lower.includes("insufficient") ||
|
|
26
|
+
lower.includes("not accessible by integration") ||
|
|
27
|
+
lower.includes("could not read from remote repository") ||
|
|
28
|
+
lower.includes("repository not found")
|
|
29
|
+
) {
|
|
30
|
+
return "GitHub access failed. Verify your token permissions and workspace repo, then try again.";
|
|
31
|
+
}
|
|
32
|
+
if (
|
|
33
|
+
lower.includes("already exists") &&
|
|
34
|
+
(lower.includes("repo") || lower.includes("repository"))
|
|
35
|
+
) {
|
|
36
|
+
return "Repository setup failed because the target repo already exists or is unavailable.";
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
lower.includes("invalid api key") ||
|
|
40
|
+
lower.includes("invalid_api_key") ||
|
|
41
|
+
lower.includes("unauthorized") ||
|
|
42
|
+
lower.includes("authentication failed") ||
|
|
43
|
+
lower.includes("invalid token")
|
|
44
|
+
) {
|
|
45
|
+
return "Model provider authentication failed. Check your API key/token and try again.";
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
lower.includes("etimedout") ||
|
|
49
|
+
lower.includes("econnreset") ||
|
|
50
|
+
lower.includes("enotfound") ||
|
|
51
|
+
lower.includes("network") ||
|
|
52
|
+
lower.includes("timed out")
|
|
53
|
+
) {
|
|
54
|
+
return "Network error during onboarding. Please retry in a minute.";
|
|
55
|
+
}
|
|
56
|
+
if (lower.includes("command failed: openclaw onboard")) {
|
|
57
|
+
return "Onboarding command failed. Please verify credentials and try again.";
|
|
58
|
+
}
|
|
59
|
+
return redacted.slice(0, 300);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const registerOnboardingRoutes = ({
|
|
63
|
+
app,
|
|
64
|
+
fs,
|
|
65
|
+
constants,
|
|
66
|
+
shellCmd,
|
|
67
|
+
gatewayEnv,
|
|
68
|
+
writeEnvFile,
|
|
69
|
+
reloadEnv,
|
|
70
|
+
isOnboarded,
|
|
71
|
+
resolveGithubRepoUrl,
|
|
72
|
+
resolveModelProvider,
|
|
73
|
+
hasCodexOauthProfile,
|
|
74
|
+
ensureGatewayProxyConfig,
|
|
75
|
+
getBaseUrl,
|
|
76
|
+
startGateway,
|
|
77
|
+
}) => {
|
|
78
|
+
const onboardingService = createOnboardingService({
|
|
79
|
+
fs,
|
|
80
|
+
constants,
|
|
81
|
+
shellCmd,
|
|
82
|
+
gatewayEnv,
|
|
83
|
+
writeEnvFile,
|
|
84
|
+
reloadEnv,
|
|
85
|
+
resolveGithubRepoUrl,
|
|
86
|
+
resolveModelProvider,
|
|
87
|
+
hasCodexOauthProfile,
|
|
88
|
+
ensureGatewayProxyConfig,
|
|
89
|
+
getBaseUrl,
|
|
90
|
+
startGateway,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
app.get("/api/onboard/status", (req, res) => {
|
|
94
|
+
res.json({ onboarded: isOnboarded() });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
app.post("/api/onboard", async (req, res) => {
|
|
98
|
+
if (isOnboarded())
|
|
99
|
+
return res.json({ ok: false, error: "Already onboarded" });
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const { vars, modelKey } = req.body;
|
|
103
|
+
const result = await onboardingService.completeOnboarding({
|
|
104
|
+
req,
|
|
105
|
+
vars,
|
|
106
|
+
modelKey,
|
|
107
|
+
});
|
|
108
|
+
res.status(result.status).json(result.body);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error("[onboard] Error:", err);
|
|
111
|
+
res.status(500).json({ ok: false, error: sanitizeOnboardingError(err) });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
module.exports = { registerOnboardingRoutes };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const registerPageRoutes = ({ app, requireAuth, isGatewayRunning }) => {
|
|
4
|
+
app.get("/health", async (req, res) => {
|
|
5
|
+
const running = await isGatewayRunning();
|
|
6
|
+
res.json({
|
|
7
|
+
status: running ? "healthy" : "starting",
|
|
8
|
+
gateway: running ? "running" : "starting",
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
app.get("/", requireAuth, (req, res) => {
|
|
13
|
+
res.sendFile(path.join(__dirname, "..", "..", "public", "setup.html"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
app.get("/setup", (req, res) => {
|
|
17
|
+
res.sendFile(path.join(__dirname, "..", "..", "public", "setup.html"));
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
module.exports = { registerPageRoutes };
|