@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.
Files changed (53) hide show
  1. package/bin/alphaclaw.js +338 -0
  2. package/lib/public/icons/chevron-down.svg +9 -0
  3. package/lib/public/js/app.js +325 -0
  4. package/lib/public/js/components/badge.js +16 -0
  5. package/lib/public/js/components/channels.js +36 -0
  6. package/lib/public/js/components/credentials-modal.js +336 -0
  7. package/lib/public/js/components/device-pairings.js +72 -0
  8. package/lib/public/js/components/envars.js +354 -0
  9. package/lib/public/js/components/gateway.js +163 -0
  10. package/lib/public/js/components/google.js +223 -0
  11. package/lib/public/js/components/icons.js +23 -0
  12. package/lib/public/js/components/models.js +461 -0
  13. package/lib/public/js/components/pairings.js +74 -0
  14. package/lib/public/js/components/scope-picker.js +106 -0
  15. package/lib/public/js/components/toast.js +31 -0
  16. package/lib/public/js/components/welcome.js +541 -0
  17. package/lib/public/js/hooks/usePolling.js +29 -0
  18. package/lib/public/js/lib/api.js +196 -0
  19. package/lib/public/js/lib/model-config.js +88 -0
  20. package/lib/public/login.html +90 -0
  21. package/lib/public/setup.html +33 -0
  22. package/lib/scripts/systemctl +56 -0
  23. package/lib/server/auth-profiles.js +101 -0
  24. package/lib/server/commands.js +84 -0
  25. package/lib/server/constants.js +282 -0
  26. package/lib/server/env.js +78 -0
  27. package/lib/server/gateway.js +262 -0
  28. package/lib/server/helpers.js +192 -0
  29. package/lib/server/login-throttle.js +86 -0
  30. package/lib/server/onboarding/cron.js +51 -0
  31. package/lib/server/onboarding/github.js +49 -0
  32. package/lib/server/onboarding/index.js +127 -0
  33. package/lib/server/onboarding/openclaw.js +171 -0
  34. package/lib/server/onboarding/validation.js +107 -0
  35. package/lib/server/onboarding/workspace.js +52 -0
  36. package/lib/server/openclaw-version.js +179 -0
  37. package/lib/server/routes/auth.js +80 -0
  38. package/lib/server/routes/codex.js +204 -0
  39. package/lib/server/routes/google.js +390 -0
  40. package/lib/server/routes/models.js +68 -0
  41. package/lib/server/routes/onboarding.js +116 -0
  42. package/lib/server/routes/pages.js +21 -0
  43. package/lib/server/routes/pairings.js +134 -0
  44. package/lib/server/routes/proxy.js +29 -0
  45. package/lib/server/routes/system.js +213 -0
  46. package/lib/server.js +161 -0
  47. package/lib/setup/core-prompts/AGENTS.md +22 -0
  48. package/lib/setup/core-prompts/TOOLS.md +18 -0
  49. package/lib/setup/env.template +19 -0
  50. package/lib/setup/gitignore +12 -0
  51. package/lib/setup/hourly-git-sync.sh +86 -0
  52. package/lib/setup/skills/control-ui/SKILL.md +70 -0
  53. package/package.json +34 -0
@@ -0,0 +1,134 @@
1
+ const fs = require("fs");
2
+ const { OPENCLAW_DIR } = require("../constants");
3
+
4
+ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openclawDir = OPENCLAW_DIR }) => {
5
+ let pairingCache = { pending: [], ts: 0 };
6
+ const PAIRING_CACHE_TTL = 10000;
7
+ const kCliAutoApproveMarkerPath = `${openclawDir}/.cli-device-auto-approved`;
8
+
9
+ const hasCliAutoApproveMarker = () => fsModule.existsSync(kCliAutoApproveMarkerPath);
10
+
11
+ const writeCliAutoApproveMarker = () => {
12
+ fsModule.mkdirSync(openclawDir, { recursive: true });
13
+ fsModule.writeFileSync(
14
+ kCliAutoApproveMarkerPath,
15
+ JSON.stringify({ approvedAt: new Date().toISOString() }, null, 2),
16
+ );
17
+ };
18
+
19
+ app.get("/api/pairings", async (req, res) => {
20
+ if (Date.now() - pairingCache.ts < PAIRING_CACHE_TTL) {
21
+ return res.json({ pending: pairingCache.pending });
22
+ }
23
+
24
+ const pending = [];
25
+ const channels = ["telegram", "discord"];
26
+
27
+ for (const ch of channels) {
28
+ try {
29
+ const config = JSON.parse(fs.readFileSync(`${OPENCLAW_DIR}/openclaw.json`, "utf8"));
30
+ if (!config.channels?.[ch]?.enabled) continue;
31
+ } catch {
32
+ continue;
33
+ }
34
+
35
+ const result = await clawCmd(`pairing list ${ch}`, { quiet: true });
36
+ if (result.ok && result.stdout) {
37
+ const lines = result.stdout.split("\n").filter((l) => l.trim());
38
+ for (const line of lines) {
39
+ const codeMatch = line.match(/([A-Z0-9]{8})/);
40
+ if (codeMatch) {
41
+ pending.push({
42
+ id: codeMatch[1],
43
+ code: codeMatch[1],
44
+ channel: ch,
45
+ });
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ pairingCache = { pending, ts: Date.now() };
52
+ res.json({ pending });
53
+ });
54
+
55
+ app.post("/api/pairings/:id/approve", async (req, res) => {
56
+ const channel = req.body.channel || "telegram";
57
+ const result = await clawCmd(`pairing approve ${channel} ${req.params.id}`);
58
+ res.json(result);
59
+ });
60
+
61
+ app.post("/api/pairings/:id/reject", async (req, res) => {
62
+ const channel = req.body.channel || "telegram";
63
+ const result = await clawCmd(`pairing reject ${channel} ${req.params.id}`);
64
+ res.json(result);
65
+ });
66
+
67
+ let devicePairingCache = { pending: [], ts: 0 };
68
+ const kDevicePairingCacheTtl = 3000;
69
+
70
+ app.get("/api/devices", async (req, res) => {
71
+ if (!isOnboarded()) return res.json({ pending: [] });
72
+ if (Date.now() - devicePairingCache.ts < kDevicePairingCacheTtl) {
73
+ return res.json({ pending: devicePairingCache.pending });
74
+ }
75
+ const result = await clawCmd("devices list --json", { quiet: true });
76
+ if (!result.ok) return res.json({ pending: [] });
77
+ try {
78
+ const parsed = JSON.parse(result.stdout);
79
+ const pendingList = Array.isArray(parsed.pending) ? parsed.pending : [];
80
+ let autoApprovedRequestId = null;
81
+ if (!hasCliAutoApproveMarker()) {
82
+ const firstCliPending = pendingList.find((d) => {
83
+ const clientId = String(d.clientId || "").toLowerCase();
84
+ const clientMode = String(d.clientMode || "").toLowerCase();
85
+ return clientId === "cli" || clientMode === "cli";
86
+ });
87
+ const firstCliPendingId = firstCliPending?.requestId || firstCliPending?.id;
88
+ if (firstCliPendingId) {
89
+ console.log(`[wrapper] Auto-approving first CLI device request: ${firstCliPendingId}`);
90
+ const approveResult = await clawCmd(`devices approve ${firstCliPendingId}`, {
91
+ quiet: true,
92
+ });
93
+ if (approveResult.ok) {
94
+ writeCliAutoApproveMarker();
95
+ autoApprovedRequestId = String(firstCliPendingId);
96
+ } else {
97
+ console.log(
98
+ `[wrapper] CLI auto-approve failed: ${(approveResult.stderr || "").slice(0, 200)}`,
99
+ );
100
+ }
101
+ }
102
+ }
103
+ const pending = pendingList
104
+ .filter((d) => String(d.requestId || d.id || "") !== autoApprovedRequestId)
105
+ .map((d) => ({
106
+ id: d.requestId || d.id,
107
+ platform: d.platform || null,
108
+ clientId: d.clientId || null,
109
+ clientMode: d.clientMode || null,
110
+ role: d.role || null,
111
+ scopes: d.scopes || [],
112
+ ts: d.ts || null,
113
+ }));
114
+ devicePairingCache = { pending, ts: Date.now() };
115
+ res.json({ pending });
116
+ } catch {
117
+ res.json({ pending: [] });
118
+ }
119
+ });
120
+
121
+ app.post("/api/devices/:id/approve", async (req, res) => {
122
+ const result = await clawCmd(`devices approve ${req.params.id}`);
123
+ devicePairingCache.ts = 0;
124
+ res.json(result);
125
+ });
126
+
127
+ app.post("/api/devices/:id/reject", async (req, res) => {
128
+ const result = await clawCmd(`devices reject ${req.params.id}`);
129
+ devicePairingCache.ts = 0;
130
+ res.json(result);
131
+ });
132
+ };
133
+
134
+ module.exports = { registerPairingRoutes };
@@ -0,0 +1,29 @@
1
+ const registerProxyRoutes = ({ app, proxy, SETUP_API_PREFIXES }) => {
2
+ app.all("/openclaw", (req, res) => {
3
+ req.url = "/";
4
+ proxy.web(req, res);
5
+ });
6
+ app.all("/openclaw/*", (req, res) => {
7
+ req.url = req.url.replace(/^\/openclaw/, "");
8
+ proxy.web(req, res);
9
+ });
10
+ app.all("/assets/*", (req, res) => proxy.web(req, res));
11
+
12
+ app.all("/webhook/*", (req, res) => {
13
+ if (!req.headers.authorization && req.query.token) {
14
+ req.headers.authorization = `Bearer ${req.query.token}`;
15
+ delete req.query.token;
16
+ const url = new URL(req.url, `http://${req.headers.host}`);
17
+ url.searchParams.delete("token");
18
+ req.url = url.pathname + url.search;
19
+ }
20
+ proxy.web(req, res);
21
+ });
22
+
23
+ app.all("/api/*", (req, res) => {
24
+ if (SETUP_API_PREFIXES.some((p) => req.path.startsWith(p))) return;
25
+ proxy.web(req, res);
26
+ });
27
+ };
28
+
29
+ module.exports = { registerProxyRoutes };
@@ -0,0 +1,213 @@
1
+ const registerSystemRoutes = ({
2
+ app,
3
+ fs,
4
+ readEnvFile,
5
+ writeEnvFile,
6
+ reloadEnv,
7
+ kKnownVars,
8
+ kKnownKeys,
9
+ kSystemVars,
10
+ syncChannelConfig,
11
+ isGatewayRunning,
12
+ isOnboarded,
13
+ getChannelStatus,
14
+ openclawVersionService,
15
+ clawCmd,
16
+ restartGateway,
17
+ OPENCLAW_DIR,
18
+ }) => {
19
+ let envRestartPending = false;
20
+ const kEnvVarsHiddenFromEnvarsTab = new Set(["GITHUB_WORKSPACE_REPO"]);
21
+ const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
22
+ const kSystemCronConfigPath = `${OPENCLAW_DIR}/cron/system-sync.json`;
23
+ const kSystemCronScriptPath = `${OPENCLAW_DIR}/hourly-git-sync.sh`;
24
+ const kDefaultSystemCronSchedule = "0 * * * *";
25
+ const isValidCronSchedule = (value) =>
26
+ typeof value === "string" && /^(\S+\s+){4}\S+$/.test(value.trim());
27
+ const buildSystemCronContent = (schedule) =>
28
+ [
29
+ "SHELL=/bin/bash",
30
+ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
31
+ `${schedule} root bash "${kSystemCronScriptPath}" >> /var/log/openclaw-hourly-sync.log 2>&1`,
32
+ "",
33
+ ].join("\n");
34
+ const readSystemCronConfig = () => {
35
+ try {
36
+ const raw = fs.readFileSync(kSystemCronConfigPath, "utf8");
37
+ const parsed = JSON.parse(raw);
38
+ const enabled = parsed.enabled !== false;
39
+ const schedule = isValidCronSchedule(parsed.schedule)
40
+ ? parsed.schedule.trim()
41
+ : kDefaultSystemCronSchedule;
42
+ return { enabled, schedule };
43
+ } catch {
44
+ return { enabled: true, schedule: kDefaultSystemCronSchedule };
45
+ }
46
+ };
47
+ const getSystemCronStatus = () => {
48
+ const config = readSystemCronConfig();
49
+ return {
50
+ enabled: config.enabled,
51
+ schedule: config.schedule,
52
+ installed: fs.existsSync(kSystemCronPath),
53
+ scriptExists: fs.existsSync(kSystemCronScriptPath),
54
+ };
55
+ };
56
+ const applySystemCronConfig = (nextConfig) => {
57
+ fs.mkdirSync(`${OPENCLAW_DIR}/cron`, { recursive: true });
58
+ fs.writeFileSync(kSystemCronConfigPath, JSON.stringify(nextConfig, null, 2));
59
+ if (nextConfig.enabled) {
60
+ fs.writeFileSync(kSystemCronPath, buildSystemCronContent(nextConfig.schedule), {
61
+ mode: 0o644,
62
+ });
63
+ } else {
64
+ fs.rmSync(kSystemCronPath, { force: true });
65
+ }
66
+ return getSystemCronStatus();
67
+ };
68
+
69
+ app.get("/api/env", (req, res) => {
70
+ const fileVars = readEnvFile();
71
+ const merged = [];
72
+
73
+ for (const def of kKnownVars) {
74
+ if (kEnvVarsHiddenFromEnvarsTab.has(def.key)) continue;
75
+ const fileEntry = fileVars.find((v) => v.key === def.key);
76
+ const value = fileEntry?.value || "";
77
+ merged.push({
78
+ key: def.key,
79
+ value,
80
+ label: def.label,
81
+ group: def.group,
82
+ hint: def.hint,
83
+ source: fileEntry?.value ? "env_file" : "unset",
84
+ editable: true,
85
+ });
86
+ }
87
+
88
+ for (const v of fileVars) {
89
+ if (kKnownKeys.has(v.key) || kSystemVars.has(v.key)) continue;
90
+ merged.push({
91
+ key: v.key,
92
+ value: v.value,
93
+ label: v.key,
94
+ group: "custom",
95
+ hint: "",
96
+ source: "env_file",
97
+ editable: true,
98
+ });
99
+ }
100
+
101
+ res.json({ vars: merged, restartRequired: envRestartPending && isOnboarded() });
102
+ });
103
+
104
+ app.put("/api/env", (req, res) => {
105
+ const { vars } = req.body;
106
+ if (!Array.isArray(vars)) {
107
+ return res.status(400).json({ ok: false, error: "Missing vars array" });
108
+ }
109
+
110
+ const filtered = vars.filter(
111
+ (v) =>
112
+ !kSystemVars.has(v.key) &&
113
+ !kEnvVarsHiddenFromEnvarsTab.has(v.key),
114
+ );
115
+ const existingLockedVars = readEnvFile().filter((v) =>
116
+ kEnvVarsHiddenFromEnvarsTab.has(v.key),
117
+ );
118
+ const nextEnvVars = [...filtered, ...existingLockedVars];
119
+ syncChannelConfig(nextEnvVars, "remove");
120
+ writeEnvFile(nextEnvVars);
121
+ const changed = reloadEnv();
122
+ if (changed && isOnboarded()) {
123
+ envRestartPending = true;
124
+ }
125
+ const restartRequired = envRestartPending && isOnboarded();
126
+ console.log(`[wrapper] Env vars saved (${nextEnvVars.length} vars, changed=${changed})`);
127
+ syncChannelConfig(nextEnvVars, "add");
128
+
129
+ res.json({ ok: true, changed, restartRequired });
130
+ });
131
+
132
+ app.get("/api/status", async (req, res) => {
133
+ const configExists = fs.existsSync(`${OPENCLAW_DIR}/openclaw.json`);
134
+ const running = await isGatewayRunning();
135
+ const repo = process.env.GITHUB_WORKSPACE_REPO || "";
136
+ const openclawVersion = openclawVersionService.readOpenclawVersion();
137
+ res.json({
138
+ gateway: running ? "running" : configExists ? "starting" : "not_onboarded",
139
+ configExists,
140
+ channels: getChannelStatus(),
141
+ repo,
142
+ openclawVersion,
143
+ syncCron: getSystemCronStatus(),
144
+ });
145
+ });
146
+
147
+ app.get("/api/sync-cron", (req, res) => {
148
+ res.json({ ok: true, ...getSystemCronStatus() });
149
+ });
150
+
151
+ app.put("/api/sync-cron", (req, res) => {
152
+ const current = readSystemCronConfig();
153
+ const { enabled, schedule } = req.body || {};
154
+ if (enabled !== undefined && typeof enabled !== "boolean") {
155
+ return res.status(400).json({ ok: false, error: "enabled must be a boolean" });
156
+ }
157
+ if (schedule !== undefined && !isValidCronSchedule(schedule)) {
158
+ return res.status(400).json({ ok: false, error: "schedule must be a 5-field cron string" });
159
+ }
160
+ const nextConfig = {
161
+ enabled: typeof enabled === "boolean" ? enabled : current.enabled,
162
+ schedule:
163
+ typeof schedule === "string" && schedule.trim()
164
+ ? schedule.trim()
165
+ : current.schedule,
166
+ };
167
+ const status = applySystemCronConfig(nextConfig);
168
+ res.json({ ok: true, syncCron: status });
169
+ });
170
+
171
+ app.get("/api/openclaw/version", async (req, res) => {
172
+ const refresh = String(req.query.refresh || "") === "1";
173
+ const status = await openclawVersionService.getVersionStatus(refresh);
174
+ res.json(status);
175
+ });
176
+
177
+ app.post("/api/openclaw/update", async (req, res) => {
178
+ console.log("[wrapper] /api/openclaw/update requested");
179
+ const result = await openclawVersionService.updateOpenclaw();
180
+ console.log(
181
+ `[wrapper] /api/openclaw/update result: status=${result.status} ok=${result.body?.ok === true}`,
182
+ );
183
+ res.status(result.status).json(result.body);
184
+ });
185
+
186
+ app.get("/api/gateway-status", async (req, res) => {
187
+ const result = await clawCmd("status");
188
+ res.json(result);
189
+ });
190
+
191
+ app.get("/api/gateway/dashboard", async (req, res) => {
192
+ if (!isOnboarded()) return res.json({ ok: false, url: "/openclaw" });
193
+ const result = await clawCmd("dashboard --no-open");
194
+ if (result.ok && result.stdout) {
195
+ const tokenMatch = result.stdout.match(/#token=([a-zA-Z0-9]+)/);
196
+ if (tokenMatch) {
197
+ return res.json({ ok: true, url: `/openclaw/#token=${tokenMatch[1]}` });
198
+ }
199
+ }
200
+ res.json({ ok: true, url: "/openclaw" });
201
+ });
202
+
203
+ app.post("/api/gateway/restart", (req, res) => {
204
+ if (!isOnboarded()) {
205
+ return res.status(400).json({ ok: false, error: "Not onboarded" });
206
+ }
207
+ restartGateway();
208
+ envRestartPending = false;
209
+ res.json({ ok: true });
210
+ });
211
+ };
212
+
213
+ module.exports = { registerSystemRoutes };
package/lib/server.js ADDED
@@ -0,0 +1,161 @@
1
+ const express = require("express");
2
+ const http = require("http");
3
+ const httpProxy = require("http-proxy");
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+
7
+ const constants = require("./server/constants");
8
+ const {
9
+ parseJsonFromNoisyOutput,
10
+ normalizeOnboardingModels,
11
+ resolveModelProvider,
12
+ resolveGithubRepoUrl,
13
+ createPkcePair,
14
+ parseCodexAuthorizationInput,
15
+ getCodexAccountId,
16
+ getBaseUrl,
17
+ getApiEnableUrl,
18
+ readGoogleCredentials,
19
+ getClientKey,
20
+ } = require("./server/helpers");
21
+ const { readEnvFile, writeEnvFile, reloadEnv, startEnvWatcher } = require("./server/env");
22
+ const {
23
+ gatewayEnv,
24
+ isOnboarded,
25
+ isGatewayRunning,
26
+ startGateway,
27
+ restartGateway: restartGatewayWithReload,
28
+ attachGatewaySignalHandlers,
29
+ ensureGatewayProxyConfig,
30
+ syncChannelConfig,
31
+ getChannelStatus,
32
+ } = require("./server/gateway");
33
+ const { createCommands } = require("./server/commands");
34
+ const { createAuthProfiles } = require("./server/auth-profiles");
35
+ const { createLoginThrottle } = require("./server/login-throttle");
36
+ const { createOpenclawVersionService } = require("./server/openclaw-version");
37
+ const { syncBootstrapPromptFiles } = require("./server/onboarding/workspace");
38
+
39
+ const { registerAuthRoutes } = require("./server/routes/auth");
40
+ const { registerPageRoutes } = require("./server/routes/pages");
41
+ const { registerModelRoutes } = require("./server/routes/models");
42
+ const { registerOnboardingRoutes } = require("./server/routes/onboarding");
43
+ const { registerSystemRoutes } = require("./server/routes/system");
44
+ const { registerPairingRoutes } = require("./server/routes/pairings");
45
+ const { registerCodexRoutes } = require("./server/routes/codex");
46
+ const { registerGoogleRoutes } = require("./server/routes/google");
47
+ const { registerProxyRoutes } = require("./server/routes/proxy");
48
+
49
+ const { PORT, GATEWAY_URL, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
50
+
51
+ startEnvWatcher();
52
+ attachGatewaySignalHandlers();
53
+
54
+ const app = express();
55
+ app.set("trust proxy", kTrustProxyHops);
56
+ app.use(express.json());
57
+
58
+ const proxy = httpProxy.createProxyServer({
59
+ target: GATEWAY_URL,
60
+ ws: true,
61
+ changeOrigin: true,
62
+ });
63
+ proxy.on("error", (err, req, res) => {
64
+ if (res && res.writeHead) {
65
+ res.writeHead(502, { "Content-Type": "application/json" });
66
+ res.end(JSON.stringify({ error: "Gateway unavailable" }));
67
+ }
68
+ });
69
+
70
+ const authProfiles = createAuthProfiles();
71
+ const loginThrottle = { ...createLoginThrottle(), getClientKey };
72
+ const { shellCmd, clawCmd, gogCmd } = createCommands({ gatewayEnv });
73
+ const restartGateway = () => restartGatewayWithReload(reloadEnv);
74
+ const openclawVersionService = createOpenclawVersionService({
75
+ gatewayEnv,
76
+ restartGateway,
77
+ isOnboarded,
78
+ });
79
+
80
+ const { requireAuth } = registerAuthRoutes({ app, loginThrottle });
81
+ app.use(express.static(path.join(__dirname, "public")));
82
+
83
+ registerPageRoutes({ app, requireAuth, isGatewayRunning });
84
+ registerModelRoutes({
85
+ app,
86
+ shellCmd,
87
+ gatewayEnv,
88
+ parseJsonFromNoisyOutput,
89
+ normalizeOnboardingModels,
90
+ });
91
+ registerOnboardingRoutes({
92
+ app,
93
+ fs,
94
+ constants,
95
+ shellCmd,
96
+ gatewayEnv,
97
+ writeEnvFile,
98
+ reloadEnv,
99
+ isOnboarded,
100
+ resolveGithubRepoUrl,
101
+ resolveModelProvider,
102
+ hasCodexOauthProfile: authProfiles.hasCodexOauthProfile,
103
+ ensureGatewayProxyConfig,
104
+ getBaseUrl,
105
+ startGateway,
106
+ });
107
+ registerSystemRoutes({
108
+ app,
109
+ fs,
110
+ readEnvFile,
111
+ writeEnvFile,
112
+ reloadEnv,
113
+ kKnownVars: constants.kKnownVars,
114
+ kKnownKeys: constants.kKnownKeys,
115
+ kSystemVars: constants.kSystemVars,
116
+ syncChannelConfig,
117
+ isGatewayRunning,
118
+ isOnboarded,
119
+ getChannelStatus,
120
+ openclawVersionService,
121
+ clawCmd,
122
+ restartGateway,
123
+ OPENCLAW_DIR: constants.OPENCLAW_DIR,
124
+ });
125
+ registerPairingRoutes({ app, clawCmd, isOnboarded });
126
+ registerCodexRoutes({
127
+ app,
128
+ createPkcePair,
129
+ parseCodexAuthorizationInput,
130
+ getCodexAccountId,
131
+ authProfiles,
132
+ });
133
+ registerGoogleRoutes({
134
+ app,
135
+ fs,
136
+ isGatewayRunning,
137
+ gogCmd,
138
+ getBaseUrl,
139
+ readGoogleCredentials,
140
+ getApiEnableUrl,
141
+ constants,
142
+ });
143
+ registerProxyRoutes({ app, proxy, SETUP_API_PREFIXES });
144
+
145
+ const server = http.createServer(app);
146
+ server.on("upgrade", (req, socket, head) => {
147
+ proxy.ws(req, socket, head);
148
+ });
149
+
150
+ server.listen(PORT, "0.0.0.0", () => {
151
+ console.log(`[wrapper] Express listening on :${PORT}`);
152
+ syncBootstrapPromptFiles({ fs, workspaceDir: constants.WORKSPACE_DIR });
153
+ if (isOnboarded()) {
154
+ reloadEnv();
155
+ syncChannelConfig(readEnvFile());
156
+ ensureGatewayProxyConfig(null);
157
+ startGateway();
158
+ } else {
159
+ console.log("[wrapper] Awaiting onboarding via Setup UI");
160
+ }
161
+ });
@@ -0,0 +1,22 @@
1
+ ### ⚠️ No YOLO System Changes!
2
+
3
+ **NEVER** make risky system changes (OpenClaw config, network settings, package installations/updates, source code modifications, etc.) without the user's explicit approval FIRST.
4
+
5
+ Always explain:
6
+
7
+ 1. **What** you want to change
8
+ 2. **Why** you want to change it
9
+ 3. **What could go wrong**
10
+
11
+ Then WAIT for the user's approval.
12
+
13
+ ### Show Your Work (IMPORTANT)
14
+
15
+ Mandatory: Anytime you add, edit, or remove files/resources, end your message with a **Changes committed** summary.
16
+
17
+ Use workspace-relative paths only for local files (no absolute paths). Include all internal resources (files, config, cron jobs, skills) and external resources (third-party pages, databases, integrations) that were created, modified, or removed.
18
+
19
+ ```
20
+ Changes committed ([abc1234](commit url)): <-- linked commit hash
21
+ • path/or/resource (new|edit|delete) — brief description
22
+ ```
@@ -0,0 +1,18 @@
1
+ ## Git Discipline
2
+
3
+ **Commit and push after every set of changes.** Your entire .openclaw directory (config, cron, workspace) is version controlled. This is how your work survives container restarts.
4
+
5
+ ```bash
6
+ cd /data/.openclaw && git add -A && git commit -m "description" && git push
7
+ ```
8
+
9
+ Never force push. Always pull before pushing if there might be remote changes.
10
+ After pushing, include a link to the commit using the abbreviated hash: [abc1234](https://github.com/owner/repo/commit/abc1234) format. No backticks.
11
+
12
+ ## Setup UI
13
+
14
+ Web-based setup UI URL: `{{SETUP_UI_URL}}`
15
+
16
+ ## Telegram Formatting
17
+
18
+ - **Links:** Use markdown syntax `[text](URL)` — HTML `<a href>` does NOT render
@@ -0,0 +1,19 @@
1
+ # OpenClaw Environment Variables
2
+ # Edit via the Setup UI or directly in this file
3
+
4
+ # --- AI Provider (at least one required) ---
5
+ ANTHROPIC_API_KEY=
6
+ ANTHROPIC_TOKEN=
7
+ OPENAI_API_KEY=
8
+ GEMINI_API_KEY=
9
+
10
+ # --- GitHub (required) ---
11
+ GITHUB_TOKEN=
12
+ GITHUB_WORKSPACE_REPO=
13
+
14
+ # --- Channels (at least one required) ---
15
+ TELEGRAM_BOT_TOKEN=
16
+ DISCORD_BOT_TOKEN=
17
+
18
+ # --- Tools (optional) ---
19
+ BRAVE_API_KEY=
@@ -0,0 +1,12 @@
1
+ # Ignore everything by default.
2
+ *
3
+
4
+ # Whitelist specific files/dirs.
5
+ !workspace/
6
+ !workspace/**
7
+ !skills/
8
+ !skills/**
9
+ !cron/
10
+ !cron/jobs.json
11
+ !openclaw.json
12
+ !.gitignore
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPO="$(cd "$(dirname "$0")" && pwd)"
5
+ cd "$REPO"
6
+
7
+ # Drop cron scheduler runtime-only churn when it is metadata/timestamp-only.
8
+ maybe_restore_if_runtime_only() {
9
+ local file="$1"
10
+ [[ -f "$file" ]] || return 0
11
+
12
+ # Only inspect when the file differs from HEAD.
13
+ if git diff --quiet -- "$file"; then
14
+ return 0
15
+ fi
16
+
17
+ if node - "$file" <<'NODE'
18
+ const fs = require('fs');
19
+ const cp = require('child_process');
20
+ const file = process.argv[2];
21
+
22
+ const sanitize = (value) => {
23
+ if (Array.isArray(value)) return value.map(sanitize);
24
+ if (value && typeof value === 'object') {
25
+ const out = {};
26
+ for (const [k, v] of Object.entries(value)) {
27
+ if (/^(lastRun|nextRun|updatedAt|createdAt|lastStarted|lastFinished|lastSuccess|lastFailure|lastError|lastExitCode|lastDurationMs|runCount|runs|timestamp|time|ts|ms)$/i.test(k)) {
28
+ continue;
29
+ }
30
+ out[k] = sanitize(v);
31
+ }
32
+ return out;
33
+ }
34
+ return value;
35
+ };
36
+
37
+ const parseJson = (str) => {
38
+ try {
39
+ return JSON.parse(str);
40
+ } catch {
41
+ return null;
42
+ }
43
+ };
44
+
45
+ let headRaw = '';
46
+ try {
47
+ headRaw = cp.execSync(`git show HEAD:${file}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
48
+ } catch {
49
+ process.exit(2); // no HEAD version to compare
50
+ }
51
+
52
+ let workRaw = '';
53
+ try {
54
+ workRaw = fs.readFileSync(file, 'utf8');
55
+ } catch {
56
+ process.exit(3);
57
+ }
58
+
59
+ const headJson = parseJson(headRaw);
60
+ const workJson = parseJson(workRaw);
61
+ if (!headJson || !workJson) process.exit(4);
62
+
63
+ const a = JSON.stringify(sanitize(headJson));
64
+ const b = JSON.stringify(sanitize(workJson));
65
+ process.exit(a === b ? 0 : 1);
66
+ NODE
67
+ then
68
+ # Runtime metadata only; restore cleanly so it doesn't create noise commits.
69
+ git restore --worktree --staged -- "$file" || git checkout -- "$file"
70
+ fi
71
+ }
72
+
73
+ maybe_restore_if_runtime_only "cron/jobs.json"
74
+ maybe_restore_if_runtime_only "crons.json"
75
+
76
+ # Stage everything else.
77
+ git add -A
78
+
79
+ # Nothing to commit? done.
80
+ if git diff --cached --quiet; then
81
+ exit 0
82
+ fi
83
+
84
+ msg="Auto-commit hourly sync $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
85
+ git commit -m "$msg"
86
+ git push