@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,36 @@
1
+ import { h } from 'https://esm.sh/preact';
2
+ import htm from 'https://esm.sh/htm';
3
+ import { Badge } from './badge.js';
4
+ const html = htm.bind(h);
5
+
6
+ const ALL_CHANNELS = ['telegram', 'discord'];
7
+
8
+ export function Channels({ channels, onSwitchTab }) {
9
+ return html`
10
+ <div class="bg-surface border border-border rounded-xl p-4">
11
+ <h2 class="font-semibold mb-3">Channels</h2>
12
+ <div class="space-y-2">
13
+ ${channels ? ALL_CHANNELS.map(ch => {
14
+ const info = channels[ch];
15
+ let badge;
16
+ if (!info) {
17
+ badge = html`<a
18
+ href="#"
19
+ onclick=${(e) => { e.preventDefault(); onSwitchTab?.('envars'); }}
20
+ class="text-xs text-gray-500 hover:text-gray-300"
21
+ >Add token</a>`;
22
+ } else if (info.status === 'paired') {
23
+ badge = html`<${Badge} tone="success">Paired (${info.paired})</${Badge}>`;
24
+ } else {
25
+ badge = html`<${Badge} tone="warning">Awaiting pairing</${Badge}>`;
26
+ }
27
+ return html`<div class="flex justify-between items-center py-1.5">
28
+ <span class="font-medium text-sm">${ch.charAt(0).toUpperCase() + ch.slice(1)}</span>
29
+ ${badge}
30
+ </div>`;
31
+ }) : html`<div class="text-gray-500 text-sm text-center py-2">Loading...</div>`}
32
+ </div>
33
+ </div>`;
34
+ }
35
+
36
+ export { ALL_CHANNELS };
@@ -0,0 +1,336 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useState, useRef } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { saveGoogleCredentials } from "../lib/api.js";
5
+ const html = htm.bind(h);
6
+
7
+ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
8
+ const [clientId, setClientId] = useState("");
9
+ const [clientSecret, setClientSecret] = useState("");
10
+ const [email, setEmail] = useState("");
11
+ const [error, setError] = useState("");
12
+ const [saving, setSaving] = useState(false);
13
+ const [instrType, setInstrType] = useState("personal");
14
+ const [redirectUriCopied, setRedirectUriCopied] = useState(false);
15
+ const fileRef = useRef(null);
16
+
17
+ if (!visible) return null;
18
+
19
+ const redirectUri = `${window.location.origin}/auth/google/callback`;
20
+
21
+ const copyRedirectUri = async () => {
22
+ try {
23
+ await navigator.clipboard.writeText(redirectUri);
24
+ setRedirectUriCopied(true);
25
+ window.setTimeout(() => setRedirectUriCopied(false), 1500);
26
+ } catch {
27
+ setError("Unable to copy redirect URI");
28
+ }
29
+ };
30
+
31
+ const handleFile = async (e) => {
32
+ const file = e.target.files[0];
33
+ if (!file) return;
34
+ try {
35
+ const text = await file.text();
36
+ const json = JSON.parse(text);
37
+ const creds = json.installed || json.web || json;
38
+ if (creds.client_id) setClientId(creds.client_id);
39
+ if (creds.client_secret) setClientSecret(creds.client_secret);
40
+ } catch {
41
+ setError("Invalid JSON file");
42
+ }
43
+ };
44
+
45
+ const submit = async () => {
46
+ setError("");
47
+ if (!clientId || !clientSecret || !email) {
48
+ setError("All fields required");
49
+ return;
50
+ }
51
+ setSaving(true);
52
+ try {
53
+ const data = await saveGoogleCredentials(clientId, clientSecret, email);
54
+ if (data.ok) {
55
+ onClose();
56
+ onSaved();
57
+ } else setError(data.error || "Failed to save credentials");
58
+ } catch {
59
+ setError("Request failed");
60
+ } finally {
61
+ setSaving(false);
62
+ }
63
+ };
64
+
65
+ const btnCls = (type) =>
66
+ `px-2 py-1 rounded text-xs ${instrType === type ? "bg-gray-700 text-gray-200" : "bg-gray-800 text-gray-400 hover:text-gray-200"}`;
67
+
68
+ const renderRedirectUriInstruction = () => html`
69
+ <div class="mt-1 flex items-center gap-2">
70
+ <input
71
+ type="text"
72
+ readonly
73
+ value=${redirectUri}
74
+ onFocus=${(e) => e.target.select()}
75
+ onclick=${(e) => e.target.select()}
76
+ class="flex-1 min-w-0 bg-black/40 border border-border rounded px-2 py-1 text-gray-300 text-xs focus:outline-none focus:border-gray-500"
77
+ />
78
+ <button
79
+ type="button"
80
+ onclick=${copyRedirectUri}
81
+ class="shrink-0 px-2 py-1 rounded border border-border text-xs text-gray-300 hover:border-gray-500"
82
+ >
83
+ ${redirectUriCopied ? "Copied" : "Copy"}
84
+ </button>
85
+ </div>
86
+ `;
87
+
88
+ return html` <div
89
+ class="fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-50"
90
+ onclick=${(e) => {
91
+ if (e.target === e.currentTarget) onClose();
92
+ }}
93
+ >
94
+ <div
95
+ class="bg-surface border border-border rounded-xl p-6 max-w-md w-full space-y-4"
96
+ >
97
+ <h2 class="text-lg font-semibold">Connect Google Workspace</h2>
98
+ <div class="space-y-3">
99
+ <div>
100
+ <p class="text-gray-400 text-sm mb-3">
101
+ You'll need a Google Cloud OAuth app.${" "}
102
+ <a
103
+ href="https://console.cloud.google.com/apis/credentials"
104
+ target="_blank"
105
+ class="text-blue-400 hover:text-blue-300"
106
+ >Create one here →</a
107
+ >
108
+ </p>
109
+ <details
110
+ class="text-sm text-gray-400 mb-3 bg-black/20 border border-border rounded-lg px-3 py-2"
111
+ >
112
+ <summary class="cursor-pointer font-medium hover:text-gray-200">
113
+ Step-by-step instructions
114
+ </summary>
115
+ <div class="mt-2 mb-2 flex gap-2">
116
+ <button
117
+ onclick=${() => setInstrType("workspace")}
118
+ class=${btnCls("workspace")}
119
+ >
120
+ Google Workspace
121
+ </button>
122
+ <button
123
+ onclick=${() => setInstrType("personal")}
124
+ class=${btnCls("personal")}
125
+ >
126
+ Personal Gmail
127
+ </button>
128
+ </div>
129
+ ${instrType === "personal"
130
+ ? html`
131
+ <div>
132
+ <ol class="list-decimal list-inside space-y-1.5 ml-1">
133
+ <li>
134
+ ${" "}
135
+ <a
136
+ href="https://console.cloud.google.com/projectcreate"
137
+ target="_blank"
138
+ class="text-blue-400 hover:text-blue-300"
139
+ >Create a Google Cloud project</a
140
+ >
141
+ (or use existing)
142
+ </li>
143
+ <li>
144
+ Go to${" "}
145
+ <a
146
+ href="https://console.cloud.google.com/auth/audience"
147
+ target="_blank"
148
+ class="text-blue-400 hover:text-blue-300"
149
+ >OAuth consent screen</a
150
+ >
151
+ → set to <strong>External</strong>
152
+ </li>
153
+ <li>
154
+ Under${" "}
155
+ <a
156
+ href="https://console.cloud.google.com/auth/audience"
157
+ target="_blank"
158
+ class="text-blue-400 hover:text-blue-300"
159
+ >Test users</a
160
+ >, <strong>add your own email</strong>
161
+ </li>
162
+ <li>
163
+ ${" "}
164
+ <a
165
+ href="https://console.cloud.google.com/apis/library"
166
+ target="_blank"
167
+ class="text-blue-400 hover:text-blue-300"
168
+ >Enable APIs</a
169
+ >
170
+ for the services you selected below
171
+ </li>
172
+ <li>
173
+ Go to${" "}
174
+ <a
175
+ href="https://console.cloud.google.com/apis/credentials"
176
+ target="_blank"
177
+ class="text-blue-400 hover:text-blue-300"
178
+ >Credentials</a
179
+ >
180
+ → Create OAuth 2.0 Client ID (Web application)
181
+ </li>
182
+ <li>
183
+ Add redirect URI:
184
+ ${renderRedirectUriInstruction()}
185
+ </li>
186
+ <li>
187
+ Copy Client ID + Secret (or download credentials JSON)
188
+ </li>
189
+ </ol>
190
+ <p class="mt-2 text-yellow-500/80">
191
+ ⚠️ App will be in "Testing" mode. Only emails added as
192
+ Test Users can sign in (up to 100).
193
+ </p>
194
+ </div>
195
+ `
196
+ : html`
197
+ <div>
198
+ <ol class="list-decimal list-inside space-y-1.5 ml-1">
199
+ <li>
200
+ ${" "}
201
+ <a
202
+ href="https://console.cloud.google.com/projectcreate"
203
+ target="_blank"
204
+ class="text-blue-400 hover:text-blue-300"
205
+ >Create a Google Cloud project</a
206
+ >
207
+ (or use existing)
208
+ </li>
209
+ <li>
210
+ Go to${" "}
211
+ <a
212
+ href="https://console.cloud.google.com/auth/audience"
213
+ target="_blank"
214
+ class="text-blue-400 hover:text-blue-300"
215
+ >OAuth consent screen</a
216
+ >
217
+ → set to <strong>Internal</strong> (Workspace only)
218
+ </li>
219
+ <li>
220
+ ${" "}
221
+ <a
222
+ href="https://console.cloud.google.com/apis/library"
223
+ target="_blank"
224
+ class="text-blue-400 hover:text-blue-300"
225
+ >Enable APIs</a
226
+ >
227
+ for the services you selected below
228
+ </li>
229
+ <li>
230
+ Go to${" "}
231
+ <a
232
+ href="https://console.cloud.google.com/apis/credentials"
233
+ target="_blank"
234
+ class="text-blue-400 hover:text-blue-300"
235
+ >Credentials</a
236
+ >
237
+ → Create OAuth 2.0 Client ID (Web application)
238
+ </li>
239
+ <li>
240
+ Add redirect URI:
241
+ ${renderRedirectUriInstruction()}
242
+ </li>
243
+ <li>
244
+ Copy Client ID + Secret (or download credentials JSON)
245
+ </li>
246
+ </ol>
247
+ <p class="mt-2 text-green-500/80">
248
+ ✓ Internal apps skip test users and verification. Only
249
+ users in your Workspace org can authorize this Google app.
250
+ </p>
251
+ </div>
252
+ `}
253
+ </details>
254
+ </div>
255
+ <div
256
+ class="bg-black/20 border border-border rounded-lg p-3 space-y-3 mt-2"
257
+ >
258
+ <div class="flex flex-col items-center text-center gap-2 py-2">
259
+ <label class="text-sm text-gray-300 font-medium"
260
+ >Upload credentials.json</label
261
+ >
262
+ <input
263
+ type="file"
264
+ ref=${fileRef}
265
+ accept=".json"
266
+ onchange=${handleFile}
267
+ class="hidden"
268
+ />
269
+ <button
270
+ type="button"
271
+ onclick=${() => fileRef.current?.click()}
272
+ class="text-sm px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
273
+ >
274
+ Choose file
275
+ </button>
276
+ </div>
277
+ <div class="flex items-center gap-3 py-1">
278
+ <div class="h-px flex-1 bg-border"></div>
279
+ <span class="text-gray-500 text-xs">or enter manually</span>
280
+ <div class="h-px flex-1 bg-border"></div>
281
+ </div>
282
+ <div>
283
+ <label class="text-sm text-gray-400 block mb-1">Client ID</label>
284
+ <input
285
+ type="text"
286
+ value=${clientId}
287
+ onInput=${(e) => setClientId(e.target.value)}
288
+ placeholder="xxxx.apps.googleusercontent.com"
289
+ class="w-full bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
290
+ />
291
+ </div>
292
+ <div>
293
+ <label class="text-sm text-gray-400 block mb-1"
294
+ >Client Secret</label
295
+ >
296
+ <input
297
+ type="password"
298
+ value=${clientSecret}
299
+ onInput=${(e) => setClientSecret(e.target.value)}
300
+ placeholder="GOCSPX-..."
301
+ class="w-full bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
302
+ />
303
+ </div>
304
+ <div>
305
+ <label class="text-sm text-gray-400 block mb-1"
306
+ >Email (Google account to authorize)</label
307
+ >
308
+ <input
309
+ type="email"
310
+ value=${email}
311
+ onInput=${(e) => setEmail(e.target.value)}
312
+ placeholder="you@gmail.com"
313
+ class="w-full bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
314
+ />
315
+ </div>
316
+ </div>
317
+ </div>
318
+ <div class="flex gap-2 pt-2">
319
+ <button
320
+ onclick=${submit}
321
+ disabled=${saving}
322
+ class="flex-1 bg-green-500 text-black font-medium py-2 rounded-lg hover:opacity-85 transition-opacity text-sm"
323
+ >
324
+ ${saving ? "Saving..." : "Connect Google"}
325
+ </button>
326
+ <button
327
+ onclick=${onClose}
328
+ class="px-4 bg-gray-800 text-gray-300 py-2 rounded-lg hover:bg-gray-700 transition-colors text-sm"
329
+ >
330
+ Cancel
331
+ </button>
332
+ </div>
333
+ ${error ? html`<div class="text-red-400 text-xs">${error}</div>` : null}
334
+ </div>
335
+ </div>`;
336
+ };
@@ -0,0 +1,72 @@
1
+ import { h } from 'https://esm.sh/preact';
2
+ import { useState } from 'https://esm.sh/preact/hooks';
3
+ import htm from 'https://esm.sh/htm';
4
+ const html = htm.bind(h);
5
+
6
+ const kModeLabels = {
7
+ webchat: 'Browser',
8
+ cli: 'CLI',
9
+ };
10
+
11
+ const formatTitle = (d) => kModeLabels[d.clientMode] || d.clientId || 'Device';
12
+
13
+ const formatSubtitle = (d) => {
14
+ const parts = [];
15
+ if (d.platform) parts.push(d.platform);
16
+ if (d.role) parts.push(d.role);
17
+ return parts.join(' · ');
18
+ };
19
+
20
+ const DeviceRow = ({ d, onApprove, onReject }) => {
21
+ const [busy, setBusy] = useState(null);
22
+
23
+ const handle = async (action) => {
24
+ setBusy(action);
25
+ try {
26
+ if (action === 'approve') await onApprove(d.id);
27
+ else await onReject(d.id);
28
+ } catch {
29
+ setBusy(null);
30
+ }
31
+ };
32
+
33
+ const title = formatTitle(d);
34
+ const subtitle = formatSubtitle(d);
35
+
36
+ if (busy === 'approve') {
37
+ return html`
38
+ <div class="bg-black/30 rounded-lg p-3 mb-2 flex items-center gap-2">
39
+ <span class="text-green-400 text-sm">Approved</span>
40
+ <span class="text-gray-500 text-xs">${title}</span>
41
+ </div>`;
42
+ }
43
+ if (busy === 'reject') {
44
+ return html`
45
+ <div class="bg-black/30 rounded-lg p-3 mb-2 flex items-center gap-2">
46
+ <span class="text-gray-400 text-sm">Rejected</span>
47
+ <span class="text-gray-500 text-xs">${title}</span>
48
+ </div>`;
49
+ }
50
+
51
+ return html`
52
+ <div class="bg-black/30 rounded-lg p-3 mb-2">
53
+ <div class="flex items-center gap-2 mb-2">
54
+ <span class="font-medium text-sm">${title}</span>
55
+ ${subtitle && html`<span class="text-xs text-gray-500">${subtitle}</span>`}
56
+ </div>
57
+ <div class="flex gap-2">
58
+ <button onclick=${() => handle('approve')} class="bg-green-500 text-black text-xs font-medium px-3 py-1.5 rounded-lg hover:opacity-85">Approve</button>
59
+ <button onclick=${() => handle('reject')} class="bg-gray-800 text-gray-300 text-xs px-3 py-1.5 rounded-lg hover:bg-gray-700">Reject</button>
60
+ </div>
61
+ </div>`;
62
+ };
63
+
64
+ export const DevicePairings = ({ pending, onApprove, onReject }) => {
65
+ if (!pending || pending.length === 0) return null;
66
+
67
+ return html`
68
+ <div class="mt-3 pt-3 border-t border-border">
69
+ <p class="text-xs text-gray-500 mb-2">Pending device pairings</p>
70
+ ${pending.map((d) => html`<${DeviceRow} key=${d.id} d=${d} onApprove=${onApprove} onReject=${onReject} />`)}
71
+ </div>`;
72
+ };