@chrysb/alphaclaw 0.4.1-beta.1 → 0.4.1-beta.2

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.
@@ -1,19 +1,11 @@
1
1
  import { h } from "https://esm.sh/preact";
2
- import { useState, useEffect, useRef } from "https://esm.sh/preact/hooks";
2
+ import { useState, useEffect } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import {
5
5
  runOnboard,
6
6
  verifyGithubOnboardingRepo,
7
7
  fetchModels,
8
- fetchCodexStatus,
9
- disconnectCodex,
10
- exchangeCodexOAuth,
11
- fetchStatus,
12
- fetchPairings,
13
- approvePairing,
14
- rejectPairing,
15
8
  } from "../lib/api.js";
16
- import { usePolling } from "../hooks/usePolling.js";
17
9
  import {
18
10
  getModelProvider,
19
11
  getFeaturedModels,
@@ -27,41 +19,46 @@ import { WelcomeHeader } from "./onboarding/welcome-header.js";
27
19
  import { WelcomeSetupStep } from "./onboarding/welcome-setup-step.js";
28
20
  import { WelcomeFormStep } from "./onboarding/welcome-form-step.js";
29
21
  import { WelcomePairingStep } from "./onboarding/welcome-pairing-step.js";
22
+ import { getPreferredPairingChannel } from "./onboarding/pairing-utils.js";
30
23
  import {
31
- getPreferredPairingChannel,
32
- isChannelPaired,
33
- } from "./onboarding/pairing-utils.js";
24
+ kOnboardingStorageKey,
25
+ kPairingChannelKey,
26
+ useWelcomeStorage,
27
+ } from "./onboarding/use-welcome-storage.js";
28
+ import { useWelcomeCodex } from "./onboarding/use-welcome-codex.js";
29
+ import { useWelcomePairing } from "./onboarding/use-welcome-pairing.js";
34
30
  const html = htm.bind(h);
35
- const kOnboardingStorageKey = "openclaw_setup";
36
- const kOnboardingStepKey = "_step";
37
- const kPairingChannelKey = "_pairingChannel";
38
31
  const kMaxOnboardingVars = 64;
39
32
  const kMaxEnvKeyLength = 128;
40
33
  const kMaxEnvValueLength = 4096;
41
34
 
42
35
  export const Welcome = ({ onComplete }) => {
43
- const [initialSetupState] = useState(() => {
44
- try {
45
- return JSON.parse(localStorage.getItem(kOnboardingStorageKey) || "{}");
46
- } catch {
47
- return {};
48
- }
49
- });
50
- const [vals, setVals] = useState(() => ({ ...initialSetupState }));
36
+ const kSetupStepIndex = kWelcomeGroups.length;
37
+ const kPairingStepIndex = kSetupStepIndex + 1;
38
+ const { vals, setVals, setValue, step, setStep, setupError, setSetupError } =
39
+ useWelcomeStorage({
40
+ kSetupStepIndex,
41
+ kPairingStepIndex,
42
+ });
51
43
  const [models, setModels] = useState([]);
52
44
  const [modelsLoading, setModelsLoading] = useState(true);
53
45
  const [modelsError, setModelsError] = useState(null);
54
46
  const [showAllModels, setShowAllModels] = useState(false);
55
- const [codexStatus, setCodexStatus] = useState({ connected: false });
56
- const [codexLoading, setCodexLoading] = useState(true);
57
- const [codexManualInput, setCodexManualInput] = useState("");
58
- const [codexExchanging, setCodexExchanging] = useState(false);
59
- const [codexAuthStarted, setCodexAuthStarted] = useState(false);
60
- const [codexAuthWaiting, setCodexAuthWaiting] = useState(false);
61
47
  const [loading, setLoading] = useState(false);
62
48
  const [githubStepLoading, setGithubStepLoading] = useState(false);
63
- const [error, setError] = useState(null);
64
- const codexPopupPollRef = useRef(null);
49
+ const [formError, setFormError] = useState(null);
50
+ const {
51
+ codexStatus,
52
+ codexLoading,
53
+ codexManualInput,
54
+ setCodexManualInput,
55
+ codexExchanging,
56
+ codexAuthStarted,
57
+ codexAuthWaiting,
58
+ startCodexAuth,
59
+ completeCodexAuth,
60
+ handleCodexDisconnect,
61
+ } = useWelcomeCodex({ setFormError });
65
62
 
66
63
  useEffect(() => {
67
64
  fetchModels()
@@ -78,50 +75,6 @@ export const Welcome = ({ onComplete }) => {
78
75
  .finally(() => setModelsLoading(false));
79
76
  }, []);
80
77
 
81
- const refreshCodexStatus = async () => {
82
- try {
83
- const status = await fetchCodexStatus();
84
- setCodexStatus(status);
85
- if (status?.connected) {
86
- setCodexAuthStarted(false);
87
- setCodexAuthWaiting(false);
88
- }
89
- } catch {
90
- setCodexStatus({ connected: false });
91
- } finally {
92
- setCodexLoading(false);
93
- }
94
- };
95
-
96
- useEffect(() => {
97
- refreshCodexStatus();
98
- }, []);
99
-
100
- useEffect(() => {
101
- const onMessage = async (e) => {
102
- if (e.data?.codex === "success") {
103
- await refreshCodexStatus();
104
- }
105
- if (e.data?.codex === "error") {
106
- setError(`Codex auth failed: ${e.data.message || "unknown error"}`);
107
- }
108
- };
109
- window.addEventListener("message", onMessage);
110
- return () => window.removeEventListener("message", onMessage);
111
- }, []);
112
-
113
- useEffect(
114
- () => () => {
115
- if (codexPopupPollRef.current) {
116
- clearInterval(codexPopupPollRef.current);
117
- codexPopupPollRef.current = null;
118
- }
119
- },
120
- [],
121
- );
122
-
123
- const set = (key, value) => setVals((prev) => ({ ...prev, [key]: value }));
124
-
125
78
  const selectedProvider = getModelProvider(vals.MODEL_KEY);
126
79
  const featuredModels = getFeaturedModels(models);
127
80
  const baseModelOptions = showAllModels
@@ -152,18 +105,6 @@ export const Welcome = ({ onComplete }) => {
152
105
  : false;
153
106
 
154
107
  const allValid = kWelcomeGroups.every((g) => g.validate(vals, { hasAi }));
155
- const kSetupStepIndex = kWelcomeGroups.length;
156
- const kPairingStepIndex = kSetupStepIndex + 1;
157
- const [step, setStep] = useState(() => {
158
- const parsedStep = Number.parseInt(
159
- String(initialSetupState?.[kOnboardingStepKey] || ""),
160
- 10,
161
- );
162
- if (!Number.isFinite(parsedStep)) return 0;
163
- return Math.max(0, Math.min(kPairingStepIndex, parsedStep));
164
- });
165
- const [pairingError, setPairingError] = useState(null);
166
- const [pairingComplete, setPairingComplete] = useState(false);
167
108
  const isSetupStep = step === kSetupStepIndex;
168
109
  const isPairingStep = step === kPairingStepIndex;
169
110
  const activeGroup = step < kSetupStepIndex ? kWelcomeGroups[step] : null;
@@ -173,94 +114,20 @@ export const Welcome = ({ onComplete }) => {
173
114
  const selectedPairingChannel = String(
174
115
  vals[kPairingChannelKey] || getPreferredPairingChannel(vals),
175
116
  );
176
- const pairingStatusPoll = usePolling(fetchStatus, 3000, {
177
- enabled: isPairingStep,
117
+ const {
118
+ pairingStatusPoll,
119
+ pairingRequestsPoll,
120
+ pairingChannels,
121
+ canFinishPairing,
122
+ pairingError,
123
+ pairingComplete,
124
+ handlePairingApprove,
125
+ handlePairingReject,
126
+ resetPairingState,
127
+ } = useWelcomePairing({
128
+ isPairingStep,
129
+ selectedPairingChannel,
178
130
  });
179
- const pairingRequestsPoll = usePolling(
180
- async () => {
181
- const payload = await fetchPairings();
182
- const allPending = payload.pending || [];
183
- return allPending.filter((p) => p.channel === selectedPairingChannel);
184
- },
185
- 1000,
186
- { enabled: isPairingStep && !!selectedPairingChannel },
187
- );
188
- const pairingChannels = pairingStatusPoll.data?.channels || {};
189
- const canFinishPairing = isChannelPaired(pairingChannels, selectedPairingChannel);
190
-
191
- useEffect(() => {
192
- if (isPairingStep && canFinishPairing) {
193
- setPairingComplete(true);
194
- }
195
- }, [isPairingStep, canFinishPairing]);
196
-
197
- useEffect(() => {
198
- localStorage.setItem(
199
- kOnboardingStorageKey,
200
- JSON.stringify({
201
- ...vals,
202
- [kOnboardingStepKey]: step,
203
- }),
204
- );
205
- }, [vals, step]);
206
-
207
- const startCodexAuth = () => {
208
- if (codexStatus.connected) return;
209
- setCodexAuthStarted(true);
210
- setCodexAuthWaiting(true);
211
- const authUrl = "/auth/codex/start";
212
- const popup = window.open(
213
- authUrl,
214
- "codex-auth",
215
- "popup=yes,width=640,height=780",
216
- );
217
- if (!popup || popup.closed) {
218
- setCodexAuthWaiting(false);
219
- window.location.href = authUrl;
220
- return;
221
- }
222
- if (codexPopupPollRef.current) {
223
- clearInterval(codexPopupPollRef.current);
224
- }
225
- codexPopupPollRef.current = setInterval(() => {
226
- if (popup.closed) {
227
- clearInterval(codexPopupPollRef.current);
228
- codexPopupPollRef.current = null;
229
- setCodexAuthWaiting(false);
230
- }
231
- }, 500);
232
- };
233
-
234
- const completeCodexAuth = async () => {
235
- if (!codexManualInput.trim() || codexExchanging) return;
236
- setCodexExchanging(true);
237
- setError(null);
238
- try {
239
- const result = await exchangeCodexOAuth(codexManualInput.trim());
240
- if (!result.ok)
241
- throw new Error(result.error || "Codex OAuth exchange failed");
242
- setCodexManualInput("");
243
- setCodexAuthStarted(false);
244
- setCodexAuthWaiting(false);
245
- await refreshCodexStatus();
246
- } catch (err) {
247
- setError(err.message || "Codex OAuth exchange failed");
248
- } finally {
249
- setCodexExchanging(false);
250
- }
251
- };
252
-
253
- const handleCodexDisconnect = async () => {
254
- const result = await disconnectCodex();
255
- if (!result.ok) {
256
- setError(result.error || "Failed to disconnect Codex");
257
- return;
258
- }
259
- setCodexAuthStarted(false);
260
- setCodexAuthWaiting(false);
261
- setCodexManualInput("");
262
- await refreshCodexStatus();
263
- };
264
131
 
265
132
  const handleSubmit = async () => {
266
133
  if (!allValid || loading) return;
@@ -294,14 +161,16 @@ export const Welcome = ({ onComplete }) => {
294
161
  return "";
295
162
  })();
296
163
  if (preflightError) {
297
- setError(preflightError);
164
+ setFormError(preflightError);
165
+ setSetupError(null);
298
166
  setStep(Math.max(0, kWelcomeGroups.findIndex((g) => g.id === "github")));
299
167
  return;
300
168
  }
301
169
  setStep(kSetupStepIndex);
302
170
  setLoading(true);
303
- setError(null);
304
- setPairingError(null);
171
+ setFormError(null);
172
+ setSetupError(null);
173
+ resetPairingState();
305
174
 
306
175
  try {
307
176
  const result = await runOnboard(vars, vals.MODEL_KEY);
@@ -316,38 +185,15 @@ export const Welcome = ({ onComplete }) => {
316
185
  }));
317
186
  setLoading(false);
318
187
  setStep(kPairingStepIndex);
319
- setPairingComplete(false);
188
+ resetPairingState();
189
+ setSetupError(null);
320
190
  } catch (err) {
321
191
  console.error("Onboard error:", err);
322
- setError(err.message);
192
+ setSetupError(err.message || "Onboarding failed");
323
193
  setLoading(false);
324
194
  }
325
195
  };
326
196
 
327
- const handlePairingApprove = async (id, channel) => {
328
- try {
329
- setPairingError(null);
330
- const result = await approvePairing(id, channel);
331
- if (!result.ok) throw new Error(result.error || "Could not approve pairing");
332
- setPairingComplete(true);
333
- pairingRequestsPoll.refresh();
334
- pairingStatusPoll.refresh();
335
- } catch (err) {
336
- setPairingError(err.message || "Could not approve pairing");
337
- }
338
- };
339
-
340
- const handlePairingReject = async (id, channel) => {
341
- try {
342
- setPairingError(null);
343
- const result = await rejectPairing(id, channel);
344
- if (!result.ok) throw new Error(result.error || "Could not reject pairing");
345
- pairingRequestsPoll.refresh();
346
- } catch (err) {
347
- setPairingError(err.message || "Could not reject pairing");
348
- }
349
- };
350
-
351
197
  const finishOnboarding = () => {
352
198
  localStorage.removeItem(kOnboardingStorageKey);
353
199
  onComplete();
@@ -355,17 +201,18 @@ export const Welcome = ({ onComplete }) => {
355
201
 
356
202
  const goBack = () => {
357
203
  if (isSetupStep) return;
358
- setError(null);
204
+ setFormError(null);
359
205
  setStep((prev) => Math.max(0, prev - 1));
360
206
  };
361
207
  const goBackFromSetupError = () => {
362
208
  setLoading(false);
209
+ setSetupError(null);
363
210
  setStep(kWelcomeGroups.length - 1);
364
211
  };
365
212
 
366
213
  const goNext = async () => {
367
214
  if (!activeGroup || !currentGroupValid) return;
368
- setError(null);
215
+ setFormError(null);
369
216
  if (activeGroup.id === "github") {
370
217
  setGithubStepLoading(true);
371
218
  try {
@@ -374,11 +221,11 @@ export const Welcome = ({ onComplete }) => {
374
221
  vals.GITHUB_TOKEN,
375
222
  );
376
223
  if (!result?.ok) {
377
- setError(result?.error || "GitHub verification failed");
224
+ setFormError(result?.error || "GitHub verification failed");
378
225
  return;
379
226
  }
380
227
  } catch (err) {
381
- setError(err?.message || "GitHub verification failed");
228
+ setFormError(err?.message || "GitHub verification failed");
382
229
  return;
383
230
  } finally {
384
231
  setGithubStepLoading(false);
@@ -412,7 +259,7 @@ export const Welcome = ({ onComplete }) => {
412
259
  <div class="bg-surface border border-border rounded-xl p-4 space-y-3">
413
260
  ${isSetupStep
414
261
  ? html`<${WelcomeSetupStep}
415
- error=${error}
262
+ error=${setupError}
416
263
  loading=${loading}
417
264
  onRetry=${handleSubmit}
418
265
  onBack=${goBackFromSetupError}
@@ -434,7 +281,7 @@ export const Welcome = ({ onComplete }) => {
434
281
  activeGroup=${activeGroup}
435
282
  vals=${vals}
436
283
  hasAi=${hasAi}
437
- setValue=${set}
284
+ setValue=${setValue}
438
285
  modelOptions=${modelOptions}
439
286
  modelsLoading=${modelsLoading}
440
287
  modelsError=${modelsError}
@@ -453,7 +300,7 @@ export const Welcome = ({ onComplete }) => {
453
300
  completeCodexAuth=${completeCodexAuth}
454
301
  codexExchanging=${codexExchanging}
455
302
  visibleAiFieldKeys=${visibleAiFieldKeys}
456
- error=${error}
303
+ error=${formError}
457
304
  step=${step}
458
305
  totalGroups=${kWelcomeGroups.length}
459
306
  currentGroupValid=${currentGroupValid}
@@ -540,6 +540,33 @@ export const saveFileContent = async (filePath, content) => {
540
540
  return parseJsonOrThrow(res, 'Could not save file');
541
541
  };
542
542
 
543
+ export const createBrowseFile = async (filePath) => {
544
+ const res = await authFetch('/api/browse/create-file', {
545
+ method: 'POST',
546
+ headers: { 'Content-Type': 'application/json' },
547
+ body: JSON.stringify({ path: String(filePath || '') }),
548
+ });
549
+ return parseJsonOrThrow(res, 'Could not create file');
550
+ };
551
+
552
+ export const createBrowseFolder = async (folderPath) => {
553
+ const res = await authFetch('/api/browse/create-folder', {
554
+ method: 'POST',
555
+ headers: { 'Content-Type': 'application/json' },
556
+ body: JSON.stringify({ path: String(folderPath || '') }),
557
+ });
558
+ return parseJsonOrThrow(res, 'Could not create folder');
559
+ };
560
+
561
+ export const moveBrowsePath = async (from, to) => {
562
+ const res = await authFetch('/api/browse/move', {
563
+ method: 'POST',
564
+ headers: { 'Content-Type': 'application/json' },
565
+ body: JSON.stringify({ from: String(from || ''), to: String(to || '') }),
566
+ });
567
+ return parseJsonOrThrow(res, 'Could not move path');
568
+ };
569
+
543
570
  export const deleteBrowseFile = async (filePath) => {
544
571
  const res = await authFetch('/api/browse/delete', {
545
572
  method: 'DELETE',
@@ -43,7 +43,9 @@ export const matchesBrowsePolicyPath = (policyPathSet, normalizedPath) => {
43
43
  for (const policyPath of policyPathSet) {
44
44
  if (
45
45
  safeNormalizedPath === policyPath ||
46
- safeNormalizedPath.endsWith(`/${policyPath}`)
46
+ safeNormalizedPath.endsWith(`/${policyPath}`) ||
47
+ safeNormalizedPath.startsWith(`${policyPath}/`) ||
48
+ safeNormalizedPath.includes(`/${policyPath}/`)
47
49
  ) {
48
50
  return true;
49
51
  }
@@ -6,7 +6,8 @@
6
6
  "lockedPaths": [
7
7
  "hooks/bootstrap/agents.md",
8
8
  "hooks/bootstrap/tools.md",
9
- "skills/control-ui/skill.md",
9
+ "skills/control-ui",
10
+ "skills/gog-cli",
10
11
  ".alphaclaw/hourly-git-sync.sh",
11
12
  ".alphaclaw/.cli-device-auto-approved"
12
13
  ]
@@ -0,0 +1,169 @@
1
+ const path = require("path");
2
+ const { readGoogleState } = require("./google-state");
3
+
4
+ const kSkillPartsDir = path.join(__dirname, "..", "setup", "skills", "gog-cli");
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const uniqueServiceLabels = (scopes) =>
11
+ Array.from(
12
+ new Set(
13
+ (scopes || [])
14
+ .map((scope) => String(scope || "").split(":")[0])
15
+ .filter(Boolean),
16
+ ),
17
+ );
18
+
19
+ const collectConnectedServices = (accounts) => {
20
+ const serviceSet = new Set();
21
+ for (const account of accounts) {
22
+ if (!account.authenticated) continue;
23
+ for (const label of uniqueServiceLabels(account.services)) {
24
+ serviceSet.add(label);
25
+ }
26
+ }
27
+ return serviceSet;
28
+ };
29
+
30
+ const kServiceDisplayNames = {
31
+ gmail: "Gmail",
32
+ calendar: "Calendar",
33
+ drive: "Drive",
34
+ sheets: "Sheets",
35
+ docs: "Docs",
36
+ tasks: "Tasks",
37
+ contacts: "Contacts",
38
+ meet: "Meet",
39
+ };
40
+
41
+ // Stable ordering for service sections
42
+ const kServiceOrder = [
43
+ "gmail",
44
+ "calendar",
45
+ "drive",
46
+ "sheets",
47
+ "docs",
48
+ "tasks",
49
+ "contacts",
50
+ "meet",
51
+ ];
52
+
53
+ const readServiceSection = (fs, service) => {
54
+ try {
55
+ return fs.readFileSync(path.join(kSkillPartsDir, `${service}.md`), "utf8");
56
+ } catch {
57
+ return null;
58
+ }
59
+ };
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Skill content builder
63
+ // ---------------------------------------------------------------------------
64
+
65
+ const buildGogSkillContent = ({ fs, accounts }) => {
66
+ const authenticatedAccounts = accounts.filter((a) => a.authenticated);
67
+ if (!authenticatedAccounts.length) return null;
68
+
69
+ const connectedServices = collectConnectedServices(authenticatedAccounts);
70
+ if (!connectedServices.size) return null;
71
+
72
+ const serviceNames = kServiceOrder
73
+ .filter((svc) => connectedServices.has(svc))
74
+ .map((svc) => kServiceDisplayNames[svc] || svc);
75
+
76
+ const lines = [];
77
+
78
+ // Frontmatter
79
+ lines.push("---");
80
+ lines.push("name: gog-cli");
81
+ lines.push(
82
+ `description: Google Workspace CLI (gog) — command reference for ${serviceNames.join(", ")}.`,
83
+ );
84
+ lines.push("---");
85
+ lines.push("");
86
+
87
+ // Header
88
+ lines.push("# gog — Google Workspace CLI");
89
+ lines.push("");
90
+ lines.push(
91
+ "Fast, script-friendly CLI for Google Workspace. All commands output structured JSON with `--json` or stable TSV with `--plain`.",
92
+ );
93
+ lines.push("");
94
+
95
+ // Global flags
96
+ lines.push("## Global Flags");
97
+ lines.push("");
98
+ lines.push("```");
99
+ lines.push("--account <email> Account to use (or set GOG_ACCOUNT)");
100
+ lines.push("--client <name> OAuth client (default: \"default\")");
101
+ lines.push("--json Structured JSON output");
102
+ lines.push("--plain Stable TSV output (no colors)");
103
+ lines.push("--force Skip confirmations");
104
+ lines.push("--verbose Verbose logging");
105
+ lines.push("```");
106
+ lines.push("");
107
+
108
+ // Account table
109
+ lines.push("## Connected Accounts");
110
+ lines.push("");
111
+ lines.push("| Email | Client | Services |");
112
+ lines.push("| ----- | ------ | -------- |");
113
+ for (const account of authenticatedAccounts) {
114
+ const email = String(account.email || "").trim() || "(unknown)";
115
+ const client = String(account.client || "default").trim();
116
+ const services = uniqueServiceLabels(account.services).join(", ");
117
+ lines.push(`| ${email} | ${client} | ${services} |`);
118
+ }
119
+ lines.push("");
120
+ lines.push(
121
+ "Always pass `--account <email>` (and `--client <name>` if not \"default\") so gog targets the correct account.",
122
+ );
123
+ lines.push("");
124
+
125
+ // Per-service sections (read from markdown files)
126
+ for (const svc of kServiceOrder) {
127
+ if (!connectedServices.has(svc)) continue;
128
+ const section = readServiceSection(fs, svc);
129
+ if (section) {
130
+ lines.push(section.trimEnd());
131
+ lines.push("");
132
+ }
133
+ }
134
+
135
+ return lines.join("\n");
136
+ };
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Installer (reads state, writes SKILL.md)
140
+ // ---------------------------------------------------------------------------
141
+
142
+ const installGogCliSkill = ({ fs, openclawDir }) => {
143
+ try {
144
+ const statePath = path.join(openclawDir, "gogcli", "state.json");
145
+ const state = readGoogleState({ fs, statePath });
146
+ const accounts = Array.isArray(state.accounts) ? state.accounts : [];
147
+ const content = buildGogSkillContent({ fs, accounts });
148
+
149
+ const skillDir = path.join(openclawDir, "skills", "gog-cli");
150
+
151
+ if (!content) {
152
+ // No authenticated accounts — remove stale skill if present
153
+ const skillPath = path.join(skillDir, "SKILL.md");
154
+ if (fs.existsSync(skillPath)) {
155
+ fs.unlinkSync(skillPath);
156
+ console.log("[gog-skill] Removed stale gog-cli skill (no connected accounts)");
157
+ }
158
+ return;
159
+ }
160
+
161
+ fs.mkdirSync(skillDir, { recursive: true });
162
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), content);
163
+ console.log("[gog-skill] gog-cli skill installed");
164
+ } catch (e) {
165
+ console.error("[gog-skill] Install error:", e.message);
166
+ }
167
+ };
168
+
169
+ module.exports = { buildGogSkillContent, installGogCliSkill };