@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,282 @@
|
|
|
1
|
+
const os = require("os");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const parsePositiveIntEnv = (value, fallbackValue) => {
|
|
5
|
+
const parsed = Number.parseInt(String(value || ""), 10);
|
|
6
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackValue;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Portable root directory: --root-dir flag sets ALPHACLAW_ROOT_DIR before require
|
|
10
|
+
const kRootDir = process.env.ALPHACLAW_ROOT_DIR
|
|
11
|
+
|| path.join(os.homedir(), ".alphaclaw");
|
|
12
|
+
const kPackageRoot = path.resolve(__dirname, "..");
|
|
13
|
+
const kSetupDir = path.join(kPackageRoot, "setup");
|
|
14
|
+
|
|
15
|
+
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
16
|
+
const GATEWAY_PORT = 18789;
|
|
17
|
+
const GATEWAY_HOST = "127.0.0.1";
|
|
18
|
+
const GATEWAY_URL = `http://${GATEWAY_HOST}:${GATEWAY_PORT}`;
|
|
19
|
+
const OPENCLAW_DIR = path.join(kRootDir, ".openclaw");
|
|
20
|
+
const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
21
|
+
const ENV_FILE_PATH = path.join(kRootDir, ".env");
|
|
22
|
+
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, "workspace");
|
|
23
|
+
const AUTH_PROFILES_PATH = path.join(OPENCLAW_DIR, "agents", "main", "agent", "auth-profiles.json");
|
|
24
|
+
const CODEX_PROFILE_ID = "openai-codex:codex-cli";
|
|
25
|
+
const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
26
|
+
const CODEX_OAUTH_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
27
|
+
const CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
28
|
+
const CODEX_OAUTH_REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
29
|
+
const CODEX_OAUTH_SCOPE = "openid profile email offline_access";
|
|
30
|
+
const CODEX_JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
31
|
+
const kCodexOauthStateTtlMs = 10 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
const kTrustProxyHops = parsePositiveIntEnv(process.env.TRUST_PROXY_HOPS, 1);
|
|
34
|
+
const kLoginWindowMs = parsePositiveIntEnv(
|
|
35
|
+
process.env.LOGIN_RATE_WINDOW_MS,
|
|
36
|
+
10 * 60 * 1000,
|
|
37
|
+
);
|
|
38
|
+
const kLoginMaxAttempts = parsePositiveIntEnv(
|
|
39
|
+
process.env.LOGIN_RATE_MAX_ATTEMPTS,
|
|
40
|
+
5,
|
|
41
|
+
);
|
|
42
|
+
const kLoginBaseLockMs = parsePositiveIntEnv(
|
|
43
|
+
process.env.LOGIN_RATE_BASE_LOCK_MS,
|
|
44
|
+
60 * 1000,
|
|
45
|
+
);
|
|
46
|
+
const kLoginMaxLockMs = parsePositiveIntEnv(
|
|
47
|
+
process.env.LOGIN_RATE_MAX_LOCK_MS,
|
|
48
|
+
15 * 60 * 1000,
|
|
49
|
+
);
|
|
50
|
+
const kLoginCleanupIntervalMs = parsePositiveIntEnv(
|
|
51
|
+
process.env.LOGIN_RATE_CLEANUP_INTERVAL_MS,
|
|
52
|
+
60 * 1000,
|
|
53
|
+
);
|
|
54
|
+
const kLoginStateTtlMs = Math.max(
|
|
55
|
+
parsePositiveIntEnv(
|
|
56
|
+
process.env.LOGIN_RATE_STATE_TTL_MS,
|
|
57
|
+
Math.max(kLoginWindowMs, kLoginMaxLockMs) * 3,
|
|
58
|
+
),
|
|
59
|
+
kLoginMaxLockMs,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const kOnboardingModelProviders = new Set([
|
|
63
|
+
"anthropic",
|
|
64
|
+
"openai",
|
|
65
|
+
"openai-codex",
|
|
66
|
+
"google",
|
|
67
|
+
]);
|
|
68
|
+
const kFallbackOnboardingModels = [
|
|
69
|
+
{
|
|
70
|
+
key: "anthropic/claude-opus-4-6",
|
|
71
|
+
provider: "anthropic",
|
|
72
|
+
label: "Claude Opus 4.6",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "anthropic/claude-sonnet-4-6",
|
|
76
|
+
provider: "anthropic",
|
|
77
|
+
label: "Claude Sonnet 4.6",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: "anthropic/claude-haiku-4-6",
|
|
81
|
+
provider: "anthropic",
|
|
82
|
+
label: "Claude Haiku 4.6",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: "openai-codex/gpt-5.3-codex",
|
|
86
|
+
provider: "openai-codex",
|
|
87
|
+
label: "Codex GPT-5.3",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: "openai/gpt-5.1-codex",
|
|
91
|
+
provider: "openai",
|
|
92
|
+
label: "OpenAI GPT-5.1 Codex",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "google/gemini-3-pro-preview",
|
|
96
|
+
provider: "google",
|
|
97
|
+
label: "Gemini 3 Pro Preview",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "google/gemini-3-flash-preview",
|
|
101
|
+
provider: "google",
|
|
102
|
+
label: "Gemini 3 Flash Preview",
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const kVersionCacheTtlMs = 60 * 1000;
|
|
107
|
+
const kLatestVersionCacheTtlMs = 10 * 60 * 1000;
|
|
108
|
+
const kOpenclawRegistryUrl = "https://registry.npmjs.org/openclaw";
|
|
109
|
+
const kAppDir = kPackageRoot;
|
|
110
|
+
|
|
111
|
+
const kSystemVars = new Set([
|
|
112
|
+
"WEBHOOK_TOKEN",
|
|
113
|
+
"OPENCLAW_GATEWAY_TOKEN",
|
|
114
|
+
"SETUP_PASSWORD",
|
|
115
|
+
"PORT",
|
|
116
|
+
]);
|
|
117
|
+
const kKnownVars = [
|
|
118
|
+
{
|
|
119
|
+
key: "ANTHROPIC_API_KEY",
|
|
120
|
+
label: "Anthropic API Key",
|
|
121
|
+
group: "models",
|
|
122
|
+
hint: "From console.anthropic.com",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: "ANTHROPIC_TOKEN",
|
|
126
|
+
label: "Anthropic Setup Token",
|
|
127
|
+
group: "models",
|
|
128
|
+
hint: "From claude setup-token",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: "OPENAI_API_KEY",
|
|
132
|
+
label: "OpenAI API Key",
|
|
133
|
+
group: "models",
|
|
134
|
+
hint: "From platform.openai.com",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: "GEMINI_API_KEY",
|
|
138
|
+
label: "Gemini API Key",
|
|
139
|
+
group: "models",
|
|
140
|
+
hint: "From aistudio.google.com",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: "GITHUB_TOKEN",
|
|
144
|
+
label: "GitHub Access Token",
|
|
145
|
+
group: "github",
|
|
146
|
+
hint: "Create one with repo scope at github.com/settings/tokens",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
key: "GITHUB_WORKSPACE_REPO",
|
|
150
|
+
label: "Workspace Repo",
|
|
151
|
+
group: "github",
|
|
152
|
+
hint: "username/repo or https://github.com/username/repo",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: "TELEGRAM_BOT_TOKEN",
|
|
156
|
+
label: "Telegram Bot Token",
|
|
157
|
+
group: "channels",
|
|
158
|
+
hint: "From @BotFather",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
key: "DISCORD_BOT_TOKEN",
|
|
162
|
+
label: "Discord Bot Token",
|
|
163
|
+
group: "channels",
|
|
164
|
+
hint: "From Discord Developer Portal",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
key: "BRAVE_API_KEY",
|
|
168
|
+
label: "Brave Search API Key",
|
|
169
|
+
group: "tools",
|
|
170
|
+
hint: "From brave.com/search/api",
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
const kKnownKeys = new Set(kKnownVars.map((v) => v.key));
|
|
174
|
+
|
|
175
|
+
const SCOPE_MAP = {
|
|
176
|
+
"gmail:read": "https://www.googleapis.com/auth/gmail.readonly",
|
|
177
|
+
"gmail:write": "https://www.googleapis.com/auth/gmail.modify",
|
|
178
|
+
"calendar:read": "https://www.googleapis.com/auth/calendar.readonly",
|
|
179
|
+
"calendar:write": "https://www.googleapis.com/auth/calendar",
|
|
180
|
+
"tasks:read": "https://www.googleapis.com/auth/tasks.readonly",
|
|
181
|
+
"tasks:write": "https://www.googleapis.com/auth/tasks",
|
|
182
|
+
"docs:read": "https://www.googleapis.com/auth/documents.readonly",
|
|
183
|
+
"docs:write": "https://www.googleapis.com/auth/documents",
|
|
184
|
+
"meet:read": "https://www.googleapis.com/auth/meetings.space.readonly",
|
|
185
|
+
"meet:write": "https://www.googleapis.com/auth/meetings.space.created",
|
|
186
|
+
"drive:read": "https://www.googleapis.com/auth/drive.readonly",
|
|
187
|
+
"drive:write": "https://www.googleapis.com/auth/drive",
|
|
188
|
+
"contacts:read": "https://www.googleapis.com/auth/contacts.readonly",
|
|
189
|
+
"contacts:write": "https://www.googleapis.com/auth/contacts",
|
|
190
|
+
"sheets:read": "https://www.googleapis.com/auth/spreadsheets.readonly",
|
|
191
|
+
"sheets:write": "https://www.googleapis.com/auth/spreadsheets",
|
|
192
|
+
};
|
|
193
|
+
const REVERSE_SCOPE_MAP = Object.fromEntries(
|
|
194
|
+
Object.entries(SCOPE_MAP).map(([k, v]) => [v, k]),
|
|
195
|
+
);
|
|
196
|
+
const BASE_SCOPES = ["openid", "https://www.googleapis.com/auth/userinfo.email"];
|
|
197
|
+
|
|
198
|
+
const GOG_CONFIG_DIR = path.join(OPENCLAW_DIR, "gogcli");
|
|
199
|
+
const GOG_CREDENTIALS_PATH = path.join(GOG_CONFIG_DIR, "credentials.json");
|
|
200
|
+
const GOG_STATE_PATH = path.join(GOG_CONFIG_DIR, "state.json");
|
|
201
|
+
const GOG_KEYRING_PASSWORD =
|
|
202
|
+
process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
|
|
203
|
+
|
|
204
|
+
const API_TEST_COMMANDS = {
|
|
205
|
+
gmail: "gmail labels list",
|
|
206
|
+
calendar: "calendar calendars",
|
|
207
|
+
tasks: "tasks lists",
|
|
208
|
+
docs: "docs info __api_check__",
|
|
209
|
+
meet: "meet spaces list",
|
|
210
|
+
drive: "drive ls",
|
|
211
|
+
contacts: "contacts list",
|
|
212
|
+
sheets: "sheets metadata __api_check__",
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const kChannelDefs = {
|
|
216
|
+
telegram: { envKey: "TELEGRAM_BOT_TOKEN" },
|
|
217
|
+
discord: { envKey: "DISCORD_BOT_TOKEN" },
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const SETUP_API_PREFIXES = [
|
|
221
|
+
"/api/status",
|
|
222
|
+
"/api/pairings",
|
|
223
|
+
"/api/google",
|
|
224
|
+
"/api/codex",
|
|
225
|
+
"/api/models",
|
|
226
|
+
"/api/gateway",
|
|
227
|
+
"/api/onboard",
|
|
228
|
+
"/api/env",
|
|
229
|
+
"/api/auth",
|
|
230
|
+
"/api/openclaw",
|
|
231
|
+
"/api/devices",
|
|
232
|
+
"/api/sync-cron",
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
kRootDir,
|
|
237
|
+
kPackageRoot,
|
|
238
|
+
kSetupDir,
|
|
239
|
+
PORT,
|
|
240
|
+
GATEWAY_PORT,
|
|
241
|
+
GATEWAY_HOST,
|
|
242
|
+
GATEWAY_URL,
|
|
243
|
+
OPENCLAW_DIR,
|
|
244
|
+
GATEWAY_TOKEN,
|
|
245
|
+
ENV_FILE_PATH,
|
|
246
|
+
WORKSPACE_DIR,
|
|
247
|
+
AUTH_PROFILES_PATH,
|
|
248
|
+
CODEX_PROFILE_ID,
|
|
249
|
+
CODEX_OAUTH_CLIENT_ID,
|
|
250
|
+
CODEX_OAUTH_AUTHORIZE_URL,
|
|
251
|
+
CODEX_OAUTH_TOKEN_URL,
|
|
252
|
+
CODEX_OAUTH_REDIRECT_URI,
|
|
253
|
+
CODEX_OAUTH_SCOPE,
|
|
254
|
+
CODEX_JWT_CLAIM_PATH,
|
|
255
|
+
kCodexOauthStateTtlMs,
|
|
256
|
+
kTrustProxyHops,
|
|
257
|
+
kLoginWindowMs,
|
|
258
|
+
kLoginMaxAttempts,
|
|
259
|
+
kLoginBaseLockMs,
|
|
260
|
+
kLoginMaxLockMs,
|
|
261
|
+
kLoginCleanupIntervalMs,
|
|
262
|
+
kLoginStateTtlMs,
|
|
263
|
+
kOnboardingModelProviders,
|
|
264
|
+
kFallbackOnboardingModels,
|
|
265
|
+
kVersionCacheTtlMs,
|
|
266
|
+
kLatestVersionCacheTtlMs,
|
|
267
|
+
kOpenclawRegistryUrl,
|
|
268
|
+
kAppDir,
|
|
269
|
+
kSystemVars,
|
|
270
|
+
kKnownVars,
|
|
271
|
+
kKnownKeys,
|
|
272
|
+
SCOPE_MAP,
|
|
273
|
+
REVERSE_SCOPE_MAP,
|
|
274
|
+
BASE_SCOPES,
|
|
275
|
+
GOG_CONFIG_DIR,
|
|
276
|
+
GOG_CREDENTIALS_PATH,
|
|
277
|
+
GOG_STATE_PATH,
|
|
278
|
+
GOG_KEYRING_PASSWORD,
|
|
279
|
+
API_TEST_COMMANDS,
|
|
280
|
+
kChannelDefs,
|
|
281
|
+
SETUP_API_PREFIXES,
|
|
282
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const { ENV_FILE_PATH, kKnownVars } = require("./constants");
|
|
3
|
+
|
|
4
|
+
const readEnvFile = () => {
|
|
5
|
+
try {
|
|
6
|
+
const content = fs.readFileSync(ENV_FILE_PATH, "utf8");
|
|
7
|
+
const vars = [];
|
|
8
|
+
for (const line of content.split("\n")) {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
11
|
+
const eqIdx = trimmed.indexOf("=");
|
|
12
|
+
if (eqIdx === -1) continue;
|
|
13
|
+
vars.push({
|
|
14
|
+
key: trimmed.slice(0, eqIdx),
|
|
15
|
+
value: trimmed.slice(eqIdx + 1),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return vars;
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const writeEnvFile = (vars) => {
|
|
25
|
+
const lines = [];
|
|
26
|
+
for (const { key, value } of vars || []) {
|
|
27
|
+
if (!key) continue;
|
|
28
|
+
lines.push(`${key}=${String(value || "")}`);
|
|
29
|
+
}
|
|
30
|
+
fs.writeFileSync(ENV_FILE_PATH, lines.join("\n"));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const reloadEnv = () => {
|
|
34
|
+
const vars = readEnvFile();
|
|
35
|
+
const fileKeys = new Set(vars.map((v) => v.key));
|
|
36
|
+
let changed = false;
|
|
37
|
+
|
|
38
|
+
for (const { key, value } of vars) {
|
|
39
|
+
if (value && value !== process.env[key]) {
|
|
40
|
+
console.log(
|
|
41
|
+
`[wrapper] Env updated: ${key}=${key.toLowerCase().includes("token") || key.toLowerCase().includes("key") || key.toLowerCase().includes("password") ? "***" : value}`,
|
|
42
|
+
);
|
|
43
|
+
process.env[key] = value;
|
|
44
|
+
changed = true;
|
|
45
|
+
} else if (!value && process.env[key]) {
|
|
46
|
+
console.log(`[wrapper] Env cleared: ${key}`);
|
|
47
|
+
delete process.env[key];
|
|
48
|
+
changed = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const allKnownKeys = kKnownVars.map((v) => v.key);
|
|
53
|
+
for (const key of allKnownKeys) {
|
|
54
|
+
if (!fileKeys.has(key) && process.env[key]) {
|
|
55
|
+
console.log(`[wrapper] Env removed: ${key}`);
|
|
56
|
+
delete process.env[key];
|
|
57
|
+
changed = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return changed;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const startEnvWatcher = () => {
|
|
65
|
+
try {
|
|
66
|
+
fs.watchFile(ENV_FILE_PATH, { interval: 2000 }, () => {
|
|
67
|
+
console.log(`[wrapper] ${ENV_FILE_PATH} changed externally, reloading...`);
|
|
68
|
+
reloadEnv();
|
|
69
|
+
});
|
|
70
|
+
} catch {}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
readEnvFile,
|
|
75
|
+
writeEnvFile,
|
|
76
|
+
reloadEnv,
|
|
77
|
+
startEnvWatcher,
|
|
78
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
const { spawn, execSync } = require("child_process");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const net = require("net");
|
|
4
|
+
const { OPENCLAW_DIR, GATEWAY_HOST, GATEWAY_PORT, kChannelDefs, kRootDir } = require("./constants");
|
|
5
|
+
|
|
6
|
+
let gatewayChild = null;
|
|
7
|
+
|
|
8
|
+
const gatewayEnv = () => ({
|
|
9
|
+
...process.env,
|
|
10
|
+
OPENCLAW_HOME: kRootDir,
|
|
11
|
+
OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
|
|
12
|
+
XDG_CONFIG_HOME: OPENCLAW_DIR,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const isOnboarded = () => fs.existsSync(`${OPENCLAW_DIR}/openclaw.json`);
|
|
16
|
+
|
|
17
|
+
const isGatewayRunning = () =>
|
|
18
|
+
new Promise((resolve) => {
|
|
19
|
+
const sock = net.createConnection(GATEWAY_PORT, GATEWAY_HOST);
|
|
20
|
+
sock.setTimeout(1000);
|
|
21
|
+
sock.on("connect", () => {
|
|
22
|
+
sock.destroy();
|
|
23
|
+
resolve(true);
|
|
24
|
+
});
|
|
25
|
+
sock.on("error", () => resolve(false));
|
|
26
|
+
sock.on("timeout", () => {
|
|
27
|
+
sock.destroy();
|
|
28
|
+
resolve(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const runGatewayCmd = (cmd) => {
|
|
33
|
+
console.log(`[wrapper] Running: openclaw gateway ${cmd}`);
|
|
34
|
+
try {
|
|
35
|
+
const out = execSync(`openclaw gateway ${cmd}`, {
|
|
36
|
+
env: gatewayEnv(),
|
|
37
|
+
timeout: 15000,
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
});
|
|
40
|
+
if (out.trim()) console.log(`[wrapper] ${out.trim()}`);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
if (e.stdout?.trim())
|
|
43
|
+
console.log(`[wrapper] gateway ${cmd} stdout: ${e.stdout.trim()}`);
|
|
44
|
+
if (e.stderr?.trim())
|
|
45
|
+
console.log(`[wrapper] gateway ${cmd} stderr: ${e.stderr.trim()}`);
|
|
46
|
+
if (!e.stdout?.trim() && !e.stderr?.trim())
|
|
47
|
+
console.log(`[wrapper] gateway ${cmd} error: ${e.message}`);
|
|
48
|
+
console.log(`[wrapper] gateway ${cmd} exit code: ${e.status}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const launchGatewayProcess = () => {
|
|
53
|
+
if (gatewayChild && gatewayChild.exitCode === null && !gatewayChild.killed) {
|
|
54
|
+
console.log("[wrapper] Managed gateway process already running — skipping launch");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const child = spawn("openclaw", ["gateway", "run"], {
|
|
58
|
+
env: gatewayEnv(),
|
|
59
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
60
|
+
});
|
|
61
|
+
gatewayChild = child;
|
|
62
|
+
child.stdout.on("data", (d) => process.stdout.write(`[gateway] ${d}`));
|
|
63
|
+
child.stderr.on("data", (d) => process.stderr.write(`[gateway] ${d}`));
|
|
64
|
+
child.on("exit", (code) => {
|
|
65
|
+
console.log(`[wrapper] Gateway launcher exited with code ${code}`);
|
|
66
|
+
if (gatewayChild === child) gatewayChild = null;
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const startGateway = async () => {
|
|
71
|
+
if (!isOnboarded()) {
|
|
72
|
+
console.log("[wrapper] Not onboarded yet — skipping gateway start");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (await isGatewayRunning()) {
|
|
76
|
+
console.log("[wrapper] Gateway already running — skipping start");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log("[wrapper] Starting openclaw gateway...");
|
|
80
|
+
launchGatewayProcess();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const restartGateway = (reloadEnv) => {
|
|
84
|
+
reloadEnv();
|
|
85
|
+
if (gatewayChild && gatewayChild.exitCode === null && !gatewayChild.killed) {
|
|
86
|
+
console.log("[wrapper] Stopping managed gateway process...");
|
|
87
|
+
try {
|
|
88
|
+
gatewayChild.kill("SIGTERM");
|
|
89
|
+
gatewayChild = null;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.log(`[wrapper] Failed to stop managed gateway process: ${e.message}`);
|
|
92
|
+
runGatewayCmd("stop");
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
runGatewayCmd("stop");
|
|
96
|
+
}
|
|
97
|
+
runGatewayCmd("install --force");
|
|
98
|
+
const launchWhenReady = async () => {
|
|
99
|
+
const waitUntil = Date.now() + 8000;
|
|
100
|
+
while (Date.now() < waitUntil) {
|
|
101
|
+
if (!(await isGatewayRunning())) break;
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
103
|
+
}
|
|
104
|
+
console.log("[wrapper] Starting openclaw gateway with refreshed environment...");
|
|
105
|
+
launchGatewayProcess();
|
|
106
|
+
};
|
|
107
|
+
void launchWhenReady();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const attachGatewaySignalHandlers = () => {
|
|
111
|
+
process.on("SIGTERM", () => {
|
|
112
|
+
runGatewayCmd("stop");
|
|
113
|
+
process.exit(0);
|
|
114
|
+
});
|
|
115
|
+
process.on("SIGINT", () => {
|
|
116
|
+
runGatewayCmd("stop");
|
|
117
|
+
process.exit(0);
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const ensureGatewayProxyConfig = (origin) => {
|
|
122
|
+
if (!isOnboarded()) return false;
|
|
123
|
+
try {
|
|
124
|
+
const configPath = `${OPENCLAW_DIR}/openclaw.json`;
|
|
125
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
126
|
+
if (!cfg.gateway) cfg.gateway = {};
|
|
127
|
+
let changed = false;
|
|
128
|
+
|
|
129
|
+
if (!Array.isArray(cfg.gateway.trustedProxies)) {
|
|
130
|
+
cfg.gateway.trustedProxies = [];
|
|
131
|
+
}
|
|
132
|
+
if (!cfg.gateway.trustedProxies.includes("127.0.0.1")) {
|
|
133
|
+
cfg.gateway.trustedProxies.push("127.0.0.1");
|
|
134
|
+
console.log("[wrapper] Added 127.0.0.1 to gateway.trustedProxies");
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (origin) {
|
|
139
|
+
if (!cfg.gateway.controlUi) cfg.gateway.controlUi = {};
|
|
140
|
+
if (!Array.isArray(cfg.gateway.controlUi.allowedOrigins)) {
|
|
141
|
+
cfg.gateway.controlUi.allowedOrigins = [];
|
|
142
|
+
}
|
|
143
|
+
if (!cfg.gateway.controlUi.allowedOrigins.includes(origin)) {
|
|
144
|
+
cfg.gateway.controlUi.allowedOrigins.push(origin);
|
|
145
|
+
console.log(`[wrapper] Added dashboard origin: ${origin}`);
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (changed) {
|
|
151
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
152
|
+
}
|
|
153
|
+
return changed;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.error(`[wrapper] ensureGatewayProxyConfig error: ${e.message}`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const syncChannelConfig = (savedVars, mode = "all") => {
|
|
161
|
+
try {
|
|
162
|
+
const configPath = `${OPENCLAW_DIR}/openclaw.json`;
|
|
163
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
164
|
+
const savedMap = Object.fromEntries(
|
|
165
|
+
savedVars.filter((v) => v.value).map((v) => [v.key, v.value]),
|
|
166
|
+
);
|
|
167
|
+
const env = gatewayEnv();
|
|
168
|
+
|
|
169
|
+
for (const [ch, def] of Object.entries(kChannelDefs)) {
|
|
170
|
+
const token = savedMap[def.envKey];
|
|
171
|
+
const isConfigured = cfg.channels?.[ch]?.enabled;
|
|
172
|
+
|
|
173
|
+
if (token && !isConfigured && (mode === "add" || mode === "all")) {
|
|
174
|
+
console.log(`[wrapper] Adding channel: ${ch}`);
|
|
175
|
+
try {
|
|
176
|
+
execSync(`openclaw channels add --channel ${ch} --token "${token}"`, {
|
|
177
|
+
env,
|
|
178
|
+
timeout: 15000,
|
|
179
|
+
encoding: "utf8",
|
|
180
|
+
});
|
|
181
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
182
|
+
if (raw.includes(token)) {
|
|
183
|
+
fs.writeFileSync(
|
|
184
|
+
configPath,
|
|
185
|
+
raw.split(token).join("${" + def.envKey + "}"),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
console.log(`[wrapper] Channel ${ch} added`);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.error(
|
|
191
|
+
`[wrapper] channels add ${ch}: ${(e.stderr || e.message || "").toString().trim().slice(0, 200)}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
} else if (
|
|
195
|
+
!token &&
|
|
196
|
+
isConfigured &&
|
|
197
|
+
(mode === "remove" || mode === "all")
|
|
198
|
+
) {
|
|
199
|
+
console.log(`[wrapper] Removing channel: ${ch}`);
|
|
200
|
+
try {
|
|
201
|
+
execSync(`openclaw channels remove --channel ${ch} --delete`, {
|
|
202
|
+
env,
|
|
203
|
+
timeout: 15000,
|
|
204
|
+
encoding: "utf8",
|
|
205
|
+
});
|
|
206
|
+
console.log(`[wrapper] Channel ${ch} removed`);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.error(
|
|
209
|
+
`[wrapper] channels remove ${ch}: ${(e.stderr || e.message || "").toString().trim().slice(0, 200)}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (e) {
|
|
215
|
+
console.error("[wrapper] syncChannelConfig error:", e.message);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const getChannelStatus = () => {
|
|
220
|
+
try {
|
|
221
|
+
const config = JSON.parse(fs.readFileSync(`${OPENCLAW_DIR}/openclaw.json`, "utf8"));
|
|
222
|
+
const credDir = `${OPENCLAW_DIR}/credentials`;
|
|
223
|
+
const channels = {};
|
|
224
|
+
|
|
225
|
+
for (const ch of ["telegram", "discord"]) {
|
|
226
|
+
if (!config.channels?.[ch]?.enabled) continue;
|
|
227
|
+
if (!process.env[kChannelDefs[ch].envKey]) continue;
|
|
228
|
+
|
|
229
|
+
let paired = 0;
|
|
230
|
+
try {
|
|
231
|
+
const files = fs
|
|
232
|
+
.readdirSync(credDir)
|
|
233
|
+
.filter((f) => f.startsWith(`${ch}-`) && f.endsWith("-allowFrom.json"));
|
|
234
|
+
for (const file of files) {
|
|
235
|
+
const data = JSON.parse(fs.readFileSync(`${credDir}/${file}`, "utf8"));
|
|
236
|
+
paired += (data.allowFrom || []).length;
|
|
237
|
+
}
|
|
238
|
+
} catch {}
|
|
239
|
+
const inlineAllowFrom = config.channels[ch]?.allowFrom;
|
|
240
|
+
if (Array.isArray(inlineAllowFrom)) paired += inlineAllowFrom.length;
|
|
241
|
+
|
|
242
|
+
channels[ch] = { status: paired > 0 ? "paired" : "configured", paired };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return channels;
|
|
246
|
+
} catch {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
gatewayEnv,
|
|
253
|
+
isOnboarded,
|
|
254
|
+
isGatewayRunning,
|
|
255
|
+
runGatewayCmd,
|
|
256
|
+
startGateway,
|
|
257
|
+
restartGateway,
|
|
258
|
+
attachGatewaySignalHandlers,
|
|
259
|
+
ensureGatewayProxyConfig,
|
|
260
|
+
syncChannelConfig,
|
|
261
|
+
getChannelStatus,
|
|
262
|
+
};
|