@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,196 @@
|
|
|
1
|
+
const authFetch = async (url, opts = {}) => {
|
|
2
|
+
const res = await fetch(url, opts);
|
|
3
|
+
if (res.status === 401) {
|
|
4
|
+
window.location.href = '/setup';
|
|
5
|
+
throw new Error('Unauthorized');
|
|
6
|
+
}
|
|
7
|
+
return res;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function fetchStatus() {
|
|
11
|
+
const res = await authFetch('/api/status');
|
|
12
|
+
return res.json();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function fetchPairings() {
|
|
16
|
+
const res = await authFetch('/api/pairings');
|
|
17
|
+
return res.json();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function approvePairing(id, channel) {
|
|
21
|
+
const res = await authFetch(`/api/pairings/${id}/approve`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify({ channel }),
|
|
25
|
+
});
|
|
26
|
+
return res.json();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function rejectPairing(id, channel) {
|
|
30
|
+
const res = await authFetch(`/api/pairings/${id}/reject`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ channel }),
|
|
34
|
+
});
|
|
35
|
+
return res.json();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function fetchGoogleStatus() {
|
|
39
|
+
const res = await authFetch('/api/google/status');
|
|
40
|
+
return res.json();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function checkGoogleApis() {
|
|
44
|
+
const res = await authFetch('/api/google/check');
|
|
45
|
+
return res.json();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function saveGoogleCredentials(clientId, clientSecret, email) {
|
|
49
|
+
const res = await authFetch('/api/google/credentials', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ clientId, clientSecret, email }),
|
|
53
|
+
});
|
|
54
|
+
return res.json();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function disconnectGoogle() {
|
|
58
|
+
const res = await authFetch('/api/google/disconnect', { method: 'POST' });
|
|
59
|
+
return res.json();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function restartGateway() {
|
|
63
|
+
const res = await authFetch('/api/gateway/restart', { method: 'POST' });
|
|
64
|
+
return res.json();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fetchDashboardUrl() {
|
|
68
|
+
const res = await authFetch('/api/gateway/dashboard');
|
|
69
|
+
return res.json();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function fetchOpenclawVersion(refresh = false) {
|
|
73
|
+
const query = refresh ? '?refresh=1' : '';
|
|
74
|
+
const res = await authFetch(`/api/openclaw/version${query}`);
|
|
75
|
+
return res.json();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function updateOpenclaw() {
|
|
79
|
+
const res = await authFetch('/api/openclaw/update', { method: 'POST' });
|
|
80
|
+
return res.json();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function fetchSyncCron() {
|
|
84
|
+
const res = await authFetch('/api/sync-cron');
|
|
85
|
+
const text = await res.text();
|
|
86
|
+
let data;
|
|
87
|
+
try {
|
|
88
|
+
data = text ? JSON.parse(text) : {};
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error(text || 'Could not parse sync cron response');
|
|
91
|
+
}
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new Error(data.error || text || `HTTP ${res.status}`);
|
|
94
|
+
}
|
|
95
|
+
return data;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function updateSyncCron(payload) {
|
|
99
|
+
const res = await authFetch('/api/sync-cron', {
|
|
100
|
+
method: 'PUT',
|
|
101
|
+
headers: { 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify(payload),
|
|
103
|
+
});
|
|
104
|
+
const text = await res.text();
|
|
105
|
+
let data;
|
|
106
|
+
try {
|
|
107
|
+
data = text ? JSON.parse(text) : {};
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error(text || 'Could not parse sync cron response');
|
|
110
|
+
}
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
throw new Error(data.error || text || `HTTP ${res.status}`);
|
|
113
|
+
}
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function fetchDevicePairings() {
|
|
118
|
+
const res = await authFetch('/api/devices');
|
|
119
|
+
return res.json();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function approveDevice(id) {
|
|
123
|
+
const res = await authFetch(`/api/devices/${id}/approve`, { method: 'POST' });
|
|
124
|
+
return res.json();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function rejectDevice(id) {
|
|
128
|
+
const res = await authFetch(`/api/devices/${id}/reject`, { method: 'POST' });
|
|
129
|
+
return res.json();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function fetchOnboardStatus() {
|
|
133
|
+
const res = await authFetch('/api/onboard/status');
|
|
134
|
+
return res.json();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function runOnboard(vars, modelKey) {
|
|
138
|
+
const res = await authFetch('/api/onboard', {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
body: JSON.stringify({ vars, modelKey }),
|
|
142
|
+
});
|
|
143
|
+
return res.json();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const fetchModels = async () => {
|
|
147
|
+
const res = await authFetch('/api/models');
|
|
148
|
+
return res.json();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const fetchModelStatus = async () => {
|
|
152
|
+
const res = await authFetch('/api/models/status');
|
|
153
|
+
return res.json();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const setPrimaryModel = async (modelKey) => {
|
|
157
|
+
const res = await authFetch('/api/models/set', {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
body: JSON.stringify({ modelKey }),
|
|
161
|
+
});
|
|
162
|
+
return res.json();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const fetchCodexStatus = async () => {
|
|
166
|
+
const res = await authFetch('/api/codex/status');
|
|
167
|
+
return res.json();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const disconnectCodex = async () => {
|
|
171
|
+
const res = await authFetch('/api/codex/disconnect', { method: 'POST' });
|
|
172
|
+
return res.json();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const exchangeCodexOAuth = async (input) => {
|
|
176
|
+
const res = await authFetch('/api/codex/exchange', {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { 'Content-Type': 'application/json' },
|
|
179
|
+
body: JSON.stringify({ input }),
|
|
180
|
+
});
|
|
181
|
+
return res.json();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export async function fetchEnvVars() {
|
|
185
|
+
const res = await authFetch('/api/env');
|
|
186
|
+
return res.json();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function saveEnvVars(vars) {
|
|
190
|
+
const res = await authFetch('/api/env', {
|
|
191
|
+
method: 'PUT',
|
|
192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({ vars }),
|
|
194
|
+
});
|
|
195
|
+
return res.json();
|
|
196
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const getModelProvider = (modelKey) => String(modelKey || "").split("/")[0] || "";
|
|
2
|
+
|
|
3
|
+
export const getAuthProviderFromModelProvider = (provider) =>
|
|
4
|
+
provider === "openai-codex" ? "openai" : provider;
|
|
5
|
+
|
|
6
|
+
export const kFeaturedModelDefs = [
|
|
7
|
+
{
|
|
8
|
+
label: "Opus 4.6",
|
|
9
|
+
preferredKeys: ["anthropic/claude-opus-4-6", "anthropic/claude-opus-4-5"],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
label: "Sonnet 4.6",
|
|
13
|
+
preferredKeys: ["anthropic/claude-sonnet-4-6", "anthropic/claude-sonnet-4-5"],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: "Codex 5.3",
|
|
17
|
+
preferredKeys: ["openai-codex/gpt-5.3-codex", "openai-codex/gpt-5.2-codex"],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: "Gemini 3",
|
|
21
|
+
preferredKeys: ["google/gemini-3-pro-preview", "google/gemini-3-flash-preview"],
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const getFeaturedModels = (allModels) => {
|
|
26
|
+
const picked = [];
|
|
27
|
+
const used = new Set();
|
|
28
|
+
kFeaturedModelDefs.forEach((def) => {
|
|
29
|
+
const found = def.preferredKeys
|
|
30
|
+
.map((key) => allModels.find((model) => model.key === key))
|
|
31
|
+
.find(Boolean);
|
|
32
|
+
if (!found || used.has(found.key)) return;
|
|
33
|
+
picked.push({ ...found, featuredLabel: def.label });
|
|
34
|
+
used.add(found.key);
|
|
35
|
+
});
|
|
36
|
+
return picked;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const kProviderAuthFields = {
|
|
40
|
+
anthropic: [
|
|
41
|
+
{
|
|
42
|
+
key: "ANTHROPIC_API_KEY",
|
|
43
|
+
label: "Anthropic API Key",
|
|
44
|
+
hint: "From console.anthropic.com — recommended",
|
|
45
|
+
placeholder: "sk-ant-...",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: "ANTHROPIC_TOKEN",
|
|
49
|
+
label: "Anthropic Setup Token",
|
|
50
|
+
hint: "From claude setup-token (uses your Claude subscription)",
|
|
51
|
+
placeholder: "Token...",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
openai: [
|
|
55
|
+
{
|
|
56
|
+
key: "OPENAI_API_KEY",
|
|
57
|
+
label: "OpenAI API Key",
|
|
58
|
+
hint: "From platform.openai.com",
|
|
59
|
+
placeholder: "sk-...",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
google: [
|
|
63
|
+
{
|
|
64
|
+
key: "GEMINI_API_KEY",
|
|
65
|
+
label: "Gemini API Key",
|
|
66
|
+
hint: "From aistudio.google.com",
|
|
67
|
+
placeholder: "AI...",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const kProviderLabels = {
|
|
73
|
+
anthropic: "Anthropic",
|
|
74
|
+
openai: "OpenAI",
|
|
75
|
+
google: "Gemini",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const kProviderOrder = ["anthropic", "openai", "google"];
|
|
79
|
+
|
|
80
|
+
export const getVisibleAiFieldKeys = (provider) => {
|
|
81
|
+
const authProvider = getAuthProviderFromModelProvider(provider);
|
|
82
|
+
const fields = kProviderAuthFields[authProvider] || [];
|
|
83
|
+
return new Set(fields.map((field) => field.key));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const kAllAiAuthFields = Object.values(kProviderAuthFields)
|
|
87
|
+
.flat()
|
|
88
|
+
.filter((field, idx, arr) => arr.findIndex((item) => item.key === field.key) === idx);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>OpenClaw Setup</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script>
|
|
9
|
+
tailwind.config = {
|
|
10
|
+
theme: {
|
|
11
|
+
extend: {
|
|
12
|
+
colors: {
|
|
13
|
+
surface: '#141414',
|
|
14
|
+
border: '#222',
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
</head>
|
|
21
|
+
<body class="bg-[#0a0a0a] text-gray-200 min-h-screen flex items-center justify-center p-4">
|
|
22
|
+
<div class="max-w-sm w-full">
|
|
23
|
+
<div class="flex items-center gap-3 mb-6">
|
|
24
|
+
<div class="text-4xl">🦞</div>
|
|
25
|
+
<div>
|
|
26
|
+
<h1 class="text-2xl font-semibold">OpenClaw Setup</h1>
|
|
27
|
+
<p class="text-gray-500 text-sm">Enter password to continue</p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<form id="login-form" class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
31
|
+
<input
|
|
32
|
+
id="password"
|
|
33
|
+
type="password"
|
|
34
|
+
placeholder="Password"
|
|
35
|
+
autofocus
|
|
36
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500"
|
|
37
|
+
/>
|
|
38
|
+
<div id="error" class="text-red-400 text-xs hidden"></div>
|
|
39
|
+
<button type="submit" class="w-full bg-white text-black text-sm font-medium px-4 py-2 rounded-lg hover:opacity-85">
|
|
40
|
+
Continue
|
|
41
|
+
</button>
|
|
42
|
+
</form>
|
|
43
|
+
</div>
|
|
44
|
+
<script>
|
|
45
|
+
const formEl = document.getElementById('login-form');
|
|
46
|
+
const submitButtonEl = formEl.querySelector('button[type="submit"]');
|
|
47
|
+
|
|
48
|
+
const setPendingState = (isPending) => {
|
|
49
|
+
submitButtonEl.disabled = isPending;
|
|
50
|
+
submitButtonEl.classList.toggle('opacity-70', isPending);
|
|
51
|
+
submitButtonEl.classList.toggle('cursor-not-allowed', isPending);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
formEl.addEventListener('submit', async (e) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
const password = document.getElementById('password').value;
|
|
57
|
+
const errorEl = document.getElementById('error');
|
|
58
|
+
errorEl.classList.add('hidden');
|
|
59
|
+
setPendingState(true);
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch('/api/auth/login', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({ password }),
|
|
65
|
+
});
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
if (data.ok) {
|
|
68
|
+
window.location.href = '/';
|
|
69
|
+
} else {
|
|
70
|
+
if (res.status === 429) {
|
|
71
|
+
const retryAfterFromHeader = Number.parseInt(res.headers.get('retry-after') || '0', 10);
|
|
72
|
+
const retryAfterSec = Number.isFinite(data.retryAfterSec) && data.retryAfterSec > 0
|
|
73
|
+
? data.retryAfterSec
|
|
74
|
+
: (Number.isFinite(retryAfterFromHeader) && retryAfterFromHeader > 0 ? retryAfterFromHeader : 30);
|
|
75
|
+
errorEl.textContent = `Too many attempts. Try again in ${retryAfterSec}s.`;
|
|
76
|
+
} else {
|
|
77
|
+
errorEl.textContent = data.error || 'Login failed';
|
|
78
|
+
}
|
|
79
|
+
errorEl.classList.remove('hidden');
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
errorEl.textContent = 'Connection error';
|
|
83
|
+
errorEl.classList.remove('hidden');
|
|
84
|
+
} finally {
|
|
85
|
+
setPendingState(false);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
</script>
|
|
89
|
+
</body>
|
|
90
|
+
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>OpenClaw Setup</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script>
|
|
9
|
+
tailwind.config = {
|
|
10
|
+
theme: {
|
|
11
|
+
extend: {
|
|
12
|
+
colors: {
|
|
13
|
+
surface: '#141414',
|
|
14
|
+
border: '#222',
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
<style>
|
|
21
|
+
::placeholder { color: #444 !important; opacity: 1 !important; }
|
|
22
|
+
::-webkit-input-placeholder { color: #444 !important; }
|
|
23
|
+
::-moz-placeholder { color: #444 !important; }
|
|
24
|
+
.scope-btn { background: #1a1a1a; color: #555; border: 1px solid #333; transition: all 0.15s; }
|
|
25
|
+
.scope-btn:hover { border-color: #555; }
|
|
26
|
+
.scope-btn.active { background: #065f46; color: #6ee7b7; border-color: #059669; }
|
|
27
|
+
</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body class="bg-[#0a0a0a] text-gray-200 min-h-screen flex justify-center pt-12 pb-8 px-4">
|
|
30
|
+
<div id="app" class="flex justify-center w-full"></div>
|
|
31
|
+
<script type="module" src="./js/app.js"></script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# systemctl shim for Docker (no real systemd available)
|
|
3
|
+
# Translates restart/stop/status into process signals so the Express wrapper handles lifecycle.
|
|
4
|
+
|
|
5
|
+
ACTION=""
|
|
6
|
+
UNIT=""
|
|
7
|
+
for arg in "$@"; do
|
|
8
|
+
case "$arg" in
|
|
9
|
+
--user|--system|--no-pager|--quiet) ;;
|
|
10
|
+
restart|stop|start|status|enable|disable|is-active|is-enabled|daemon-reload|show) ACTION="$arg" ;;
|
|
11
|
+
*) [ -n "$ACTION" ] && [ -z "$UNIT" ] && UNIT="$arg" ;;
|
|
12
|
+
esac
|
|
13
|
+
done
|
|
14
|
+
|
|
15
|
+
case "$ACTION" in
|
|
16
|
+
status)
|
|
17
|
+
if [ -z "$UNIT" ]; then
|
|
18
|
+
# Bare "systemctl --user status" — systemd availability check
|
|
19
|
+
echo "● user-session"
|
|
20
|
+
echo " Active: active (running)"
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
# Status check for a specific unit
|
|
24
|
+
if pgrep -f "openclaw gateway" >/dev/null 2>&1; then
|
|
25
|
+
echo "● ${UNIT}"
|
|
26
|
+
echo " Active: active (running)"
|
|
27
|
+
exit 0
|
|
28
|
+
else
|
|
29
|
+
echo "● ${UNIT}"
|
|
30
|
+
echo " Active: inactive (dead)"
|
|
31
|
+
exit 3
|
|
32
|
+
fi
|
|
33
|
+
;;
|
|
34
|
+
is-active)
|
|
35
|
+
if pgrep -f "openclaw gateway" >/dev/null 2>&1; then
|
|
36
|
+
echo "active"
|
|
37
|
+
exit 0
|
|
38
|
+
else
|
|
39
|
+
echo "inactive"
|
|
40
|
+
exit 3
|
|
41
|
+
fi
|
|
42
|
+
;;
|
|
43
|
+
restart|stop)
|
|
44
|
+
pkill -TERM -f "openclaw gateway" 2>/dev/null || true
|
|
45
|
+
sleep 1
|
|
46
|
+
pkill -9 -f "openclaw gateway" 2>/dev/null || true
|
|
47
|
+
rm -f /data/.openclaw/gateway.lock /data/.openclaw/gateway.pid 2>/dev/null
|
|
48
|
+
exit 0
|
|
49
|
+
;;
|
|
50
|
+
start|enable|is-enabled|disable|daemon-reload|show)
|
|
51
|
+
exit 0
|
|
52
|
+
;;
|
|
53
|
+
*)
|
|
54
|
+
exit 0
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { AUTH_PROFILES_PATH, CODEX_PROFILE_ID } = require("./constants");
|
|
4
|
+
|
|
5
|
+
const createAuthProfiles = () => {
|
|
6
|
+
const ensureAuthProfilesStore = () => {
|
|
7
|
+
let store = { version: 1, profiles: {} };
|
|
8
|
+
try {
|
|
9
|
+
if (fs.existsSync(AUTH_PROFILES_PATH)) {
|
|
10
|
+
const parsed = JSON.parse(fs.readFileSync(AUTH_PROFILES_PATH, "utf8"));
|
|
11
|
+
if (
|
|
12
|
+
parsed &&
|
|
13
|
+
typeof parsed === "object" &&
|
|
14
|
+
parsed.profiles &&
|
|
15
|
+
typeof parsed.profiles === "object"
|
|
16
|
+
) {
|
|
17
|
+
store = {
|
|
18
|
+
version: Number(parsed.version || 1),
|
|
19
|
+
profiles: parsed.profiles,
|
|
20
|
+
order: parsed.order,
|
|
21
|
+
lastGood: parsed.lastGood,
|
|
22
|
+
usageStats: parsed.usageStats,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
return store;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const saveAuthProfilesStore = (store) => {
|
|
31
|
+
fs.mkdirSync(path.dirname(AUTH_PROFILES_PATH), { recursive: true });
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
AUTH_PROFILES_PATH,
|
|
34
|
+
JSON.stringify(
|
|
35
|
+
{
|
|
36
|
+
version: Number(store.version || 1),
|
|
37
|
+
profiles: store.profiles || {},
|
|
38
|
+
order: store.order,
|
|
39
|
+
lastGood: store.lastGood,
|
|
40
|
+
usageStats: store.usageStats,
|
|
41
|
+
},
|
|
42
|
+
null,
|
|
43
|
+
2,
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const listCodexProfiles = () => {
|
|
49
|
+
const store = ensureAuthProfilesStore();
|
|
50
|
+
return Object.entries(store.profiles || {})
|
|
51
|
+
.filter(([, cred]) => cred?.provider === "openai-codex")
|
|
52
|
+
.map(([id, cred]) => ({ id, cred }));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getCodexProfile = () => {
|
|
56
|
+
const profiles = listCodexProfiles();
|
|
57
|
+
if (profiles.length === 0) return null;
|
|
58
|
+
const preferred = profiles.find((p) => p.id === CODEX_PROFILE_ID) || profiles[0];
|
|
59
|
+
return { profileId: preferred.id, ...preferred.cred };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const hasCodexOauthProfile = () => {
|
|
63
|
+
const profile = getCodexProfile();
|
|
64
|
+
return !!(profile?.access && profile?.refresh);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const upsertCodexProfile = ({ access, refresh, expires, accountId }) => {
|
|
68
|
+
const store = ensureAuthProfilesStore();
|
|
69
|
+
store.profiles[CODEX_PROFILE_ID] = {
|
|
70
|
+
type: "oauth",
|
|
71
|
+
provider: "openai-codex",
|
|
72
|
+
access,
|
|
73
|
+
refresh,
|
|
74
|
+
expires,
|
|
75
|
+
...(accountId ? { accountId } : {}),
|
|
76
|
+
};
|
|
77
|
+
saveAuthProfilesStore(store);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const removeCodexProfiles = () => {
|
|
81
|
+
const store = ensureAuthProfilesStore();
|
|
82
|
+
let changed = false;
|
|
83
|
+
for (const [id, cred] of Object.entries(store.profiles || {})) {
|
|
84
|
+
if (cred?.provider === "openai-codex") {
|
|
85
|
+
delete store.profiles[id];
|
|
86
|
+
changed = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (changed) saveAuthProfilesStore(store);
|
|
90
|
+
return changed;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
getCodexProfile,
|
|
95
|
+
hasCodexOauthProfile,
|
|
96
|
+
upsertCodexProfile,
|
|
97
|
+
removeCodexProfiles,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
module.exports = { createAuthProfiles };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const { exec } = require("child_process");
|
|
2
|
+
const { OPENCLAW_DIR, GOG_KEYRING_PASSWORD } = require("./constants");
|
|
3
|
+
|
|
4
|
+
const createCommands = ({ gatewayEnv }) => {
|
|
5
|
+
const shellCmd = (cmd, opts = {}) =>
|
|
6
|
+
new Promise((resolve, reject) => {
|
|
7
|
+
const { logStdout, ...execOpts } = opts;
|
|
8
|
+
const shouldLogStdout =
|
|
9
|
+
typeof logStdout === "boolean" ? logStdout : !cmd.includes("--json");
|
|
10
|
+
console.log(
|
|
11
|
+
`[onboard] Running: ${cmd
|
|
12
|
+
.replace(/ghp_[^\s"]+/g, "***")
|
|
13
|
+
.replace(/sk-[^\s"]+/g, "***")
|
|
14
|
+
.slice(0, 200)}`,
|
|
15
|
+
);
|
|
16
|
+
exec(cmd, { timeout: 60000, ...execOpts }, (err, stdout, stderr) => {
|
|
17
|
+
if (err) {
|
|
18
|
+
console.error(
|
|
19
|
+
`[onboard] Error: ${(stderr || err.message).slice(0, 300)}`,
|
|
20
|
+
);
|
|
21
|
+
return reject(err);
|
|
22
|
+
}
|
|
23
|
+
if (shouldLogStdout && stdout.trim()) {
|
|
24
|
+
console.log(`[onboard] ${stdout.trim().slice(0, 300)}`);
|
|
25
|
+
}
|
|
26
|
+
resolve(stdout.trim());
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const clawCmd = (cmd, { quiet = false } = {}) =>
|
|
31
|
+
new Promise((resolve) => {
|
|
32
|
+
if (!quiet) console.log(`[wrapper] Running: openclaw ${cmd}`);
|
|
33
|
+
exec(
|
|
34
|
+
`openclaw ${cmd}`,
|
|
35
|
+
{
|
|
36
|
+
env: gatewayEnv(),
|
|
37
|
+
timeout: 15000,
|
|
38
|
+
},
|
|
39
|
+
(err, stdout, stderr) => {
|
|
40
|
+
const result = {
|
|
41
|
+
ok: !err,
|
|
42
|
+
stdout: stdout.trim(),
|
|
43
|
+
stderr: stderr.trim(),
|
|
44
|
+
code: err?.code,
|
|
45
|
+
};
|
|
46
|
+
if (!quiet && !result.ok) {
|
|
47
|
+
console.log(`[wrapper] Error: ${result.stderr.slice(0, 200)}`);
|
|
48
|
+
}
|
|
49
|
+
resolve(result);
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const gogCmd = (cmd, { quiet = false } = {}) =>
|
|
55
|
+
new Promise((resolve) => {
|
|
56
|
+
if (!quiet) console.log(`[wrapper] Running: gog ${cmd}`);
|
|
57
|
+
exec(
|
|
58
|
+
`gog ${cmd}`,
|
|
59
|
+
{
|
|
60
|
+
timeout: 15000,
|
|
61
|
+
env: {
|
|
62
|
+
...process.env,
|
|
63
|
+
XDG_CONFIG_HOME: OPENCLAW_DIR,
|
|
64
|
+
GOG_KEYRING_PASSWORD,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
(err, stdout, stderr) => {
|
|
68
|
+
const result = {
|
|
69
|
+
ok: !err,
|
|
70
|
+
stdout: stdout.trim(),
|
|
71
|
+
stderr: stderr.trim(),
|
|
72
|
+
};
|
|
73
|
+
if (!quiet && !result.ok) {
|
|
74
|
+
console.log(`[wrapper] gog error: ${result.stderr.slice(0, 200)}`);
|
|
75
|
+
}
|
|
76
|
+
resolve(result);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return { shellCmd, clawCmd, gogCmd };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
module.exports = { createCommands };
|