@chrysb/alphaclaw 0.3.2 → 0.3.3

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 (36) hide show
  1. package/bin/alphaclaw.js +29 -2
  2. package/lib/cli/git-sync.js +25 -0
  3. package/lib/public/css/explorer.css +983 -0
  4. package/lib/public/css/shell.css +48 -4
  5. package/lib/public/css/theme.css +6 -1
  6. package/lib/public/icons/folder-line.svg +1 -0
  7. package/lib/public/icons/hashtag.svg +3 -0
  8. package/lib/public/icons/home-5-line.svg +1 -0
  9. package/lib/public/icons/save-fill.svg +3 -0
  10. package/lib/public/js/app.js +259 -158
  11. package/lib/public/js/components/action-button.js +12 -1
  12. package/lib/public/js/components/file-tree.js +322 -0
  13. package/lib/public/js/components/file-viewer.js +691 -0
  14. package/lib/public/js/components/icons.js +182 -0
  15. package/lib/public/js/components/sidebar-git-panel.js +149 -0
  16. package/lib/public/js/components/sidebar.js +272 -0
  17. package/lib/public/js/lib/api.js +26 -0
  18. package/lib/public/js/lib/browse-draft-state.js +109 -0
  19. package/lib/public/js/lib/file-highlighting.js +6 -0
  20. package/lib/public/js/lib/file-tree-utils.js +12 -0
  21. package/lib/public/js/lib/syntax-highlighters/css.js +124 -0
  22. package/lib/public/js/lib/syntax-highlighters/frontmatter.js +49 -0
  23. package/lib/public/js/lib/syntax-highlighters/html.js +209 -0
  24. package/lib/public/js/lib/syntax-highlighters/index.js +28 -0
  25. package/lib/public/js/lib/syntax-highlighters/javascript.js +134 -0
  26. package/lib/public/js/lib/syntax-highlighters/json.js +61 -0
  27. package/lib/public/js/lib/syntax-highlighters/markdown.js +37 -0
  28. package/lib/public/js/lib/syntax-highlighters/utils.js +13 -0
  29. package/lib/public/setup.html +1 -0
  30. package/lib/server/constants.js +1 -0
  31. package/lib/server/onboarding/workspace.js +3 -2
  32. package/lib/server/routes/browse.js +295 -0
  33. package/lib/server.js +24 -3
  34. package/lib/setup/core-prompts/TOOLS.md +3 -1
  35. package/lib/setup/skills/control-ui/SKILL.md +12 -20
  36. package/package.json +1 -1
@@ -0,0 +1,61 @@
1
+ import { escapeHtml, toLineObjects } from "./utils.js";
2
+
3
+ const tokenizeJsonLine = (line) => {
4
+ const parts = [];
5
+ const source = String(line || "");
6
+ const stringRegex = /"([^"\\]|\\.)*"/g;
7
+ let lastIndex = 0;
8
+ let match = stringRegex.exec(source);
9
+
10
+ while (match) {
11
+ const start = match.index;
12
+ const end = stringRegex.lastIndex;
13
+ const value = match[0];
14
+ const trailing = source.slice(end);
15
+ const isKey = /^\s*:/.test(trailing);
16
+
17
+ if (start > lastIndex) {
18
+ parts.push({ kind: "text", value: source.slice(lastIndex, start) });
19
+ }
20
+ parts.push({ kind: isKey ? "key" : "string", value });
21
+ lastIndex = end;
22
+ match = stringRegex.exec(source);
23
+ }
24
+
25
+ if (lastIndex < source.length) {
26
+ parts.push({ kind: "text", value: source.slice(lastIndex) });
27
+ }
28
+
29
+ if (parts.length === 0) {
30
+ return [{ kind: "text", value: source }];
31
+ }
32
+ return parts;
33
+ };
34
+
35
+ const highlightJsonTextSegment = (text) => {
36
+ let content = escapeHtml(text);
37
+ content = content.replace(
38
+ /(^|[^\w.])(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)(?=$|[^\w.])/g,
39
+ '$1<span class="hl-number">$2</span>',
40
+ );
41
+ content = content.replace(/\b(true|false)\b/g, '<span class="hl-boolean">$1</span>');
42
+ content = content.replace(/\bnull\b/g, '<span class="hl-null">null</span>');
43
+ content = content.replace(/([{}\[\],:])/g, '<span class="hl-punc">$1</span>');
44
+ return content;
45
+ };
46
+
47
+ const renderHighlightedJsonLine = (line) =>
48
+ tokenizeJsonLine(line)
49
+ .map((part) => {
50
+ if (part.kind === "key") {
51
+ return `<span class="hl-key">${escapeHtml(part.value)}</span>`;
52
+ }
53
+ if (part.kind === "string") {
54
+ return `<span class="hl-string">${escapeHtml(part.value)}</span>`;
55
+ }
56
+ return highlightJsonTextSegment(part.value);
57
+ })
58
+ .join("");
59
+
60
+ export const highlightJsonContent = (content) =>
61
+ toLineObjects(content, renderHighlightedJsonLine);
@@ -0,0 +1,37 @@
1
+ import { escapeHtml, toLineObjects } from "./utils.js";
2
+
3
+ const renderInlineMarkdown = (line) => {
4
+ let content = escapeHtml(line);
5
+ content = content.replace(/`([^`]+)`/g, '<span class="hl-string">`$1`</span>');
6
+ content = content.replace(/\*\*([^*]+)\*\*/g, '<span class="hl-bold">**$1**</span>');
7
+ content = content.replace(
8
+ /\[([^\]]+)\]\(([^)]+)\)/g,
9
+ '<span class="hl-link">[$1]($2)</span>',
10
+ );
11
+ return content;
12
+ };
13
+
14
+ const renderHighlightedMarkdownLine = (line) => {
15
+ if (/^#{1,6}\s/.test(line)) {
16
+ return `<span class="hl-heading">${escapeHtml(line)}</span>`;
17
+ }
18
+ if (/^>\s/.test(line)) {
19
+ return `<span class="hl-comment">${escapeHtml(line)}</span>`;
20
+ }
21
+ if (/^```/.test(line)) {
22
+ return `<span class="hl-meta">${escapeHtml(line)}</span>`;
23
+ }
24
+ if (/^\|[-\s|]+\|$/.test(line)) {
25
+ return `<span class="hl-meta">${escapeHtml(line)}</span>`;
26
+ }
27
+ if (/^\s*[-*]\s/.test(line)) {
28
+ return renderInlineMarkdown(line).replace(
29
+ /^(\s*)([-*])/,
30
+ '$1<span class="hl-bullet">$2</span>',
31
+ );
32
+ }
33
+ return renderInlineMarkdown(line);
34
+ };
35
+
36
+ export const highlightMarkdownContent = (content) =>
37
+ toLineObjects(content, renderHighlightedMarkdownLine);
@@ -0,0 +1,13 @@
1
+ export const escapeHtml = (value) =>
2
+ String(value || "")
3
+ .replaceAll("&", "&amp;")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;");
6
+
7
+ export const toLineObjects = (content, renderer) =>
8
+ String(content || "")
9
+ .split("\n")
10
+ .map((line, index) => ({
11
+ lineNumber: index + 1,
12
+ html: renderer(line),
13
+ }));
@@ -8,6 +8,7 @@
8
8
  <link rel="icon" type="image/svg+xml" href="./img/logo.svg">
9
9
  <link rel="stylesheet" href="./css/theme.css">
10
10
  <link rel="stylesheet" href="./css/shell.css">
11
+ <link rel="stylesheet" href="./css/explorer.css">
11
12
  <script src="https://cdn.tailwindcss.com"></script>
12
13
  <script>
13
14
  tailwind.config = {
@@ -281,6 +281,7 @@ const SETUP_API_PREFIXES = [
281
281
  "/api/google",
282
282
  "/api/codex",
283
283
  "/api/models",
284
+ "/api/browse",
284
285
  "/api/gateway",
285
286
  "/api/restart-status",
286
287
  "/api/onboard",
@@ -61,12 +61,13 @@ const syncBootstrapPromptFiles = ({ fs, workspaceDir, baseUrl }) => {
61
61
 
62
62
  const installControlUiSkill = ({ fs, openclawDir, baseUrl }) => {
63
63
  try {
64
+ const setupUiUrl = resolveSetupUiUrl(baseUrl);
64
65
  const skillDir = `${openclawDir}/skills/control-ui`;
65
66
  fs.mkdirSync(skillDir, { recursive: true });
66
67
  const skillTemplate = fs.readFileSync(path.join(kSetupDir, "skills", "control-ui", "SKILL.md"), "utf8");
67
- const skillContent = skillTemplate.replace(/\{\{BASE_URL\}\}/g, baseUrl);
68
+ const skillContent = skillTemplate.replace(/\{\{BASE_URL\}\}/g, setupUiUrl);
68
69
  fs.writeFileSync(`${skillDir}/SKILL.md`, skillContent);
69
- console.log(`[onboard] Control UI skill installed (${baseUrl})`);
70
+ console.log(`[onboard] Control UI skill installed (${setupUiUrl})`);
70
71
  } catch (e) {
71
72
  console.error("[onboard] Skill install error:", e.message);
72
73
  }
@@ -0,0 +1,295 @@
1
+ const path = require("path");
2
+ const { execFile } = require("child_process");
3
+
4
+ const kDefaultTreeDepth = 10;
5
+ const kIgnoredDirectoryNames = new Set([
6
+ ".git",
7
+ "node_modules",
8
+ ".cache",
9
+ "dist",
10
+ "build",
11
+ ]);
12
+
13
+ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
14
+ const kRootResolved = path.resolve(kRootDir);
15
+ const kRootWithSep = `${kRootResolved}${path.sep}`;
16
+ const kRootDisplayName = "kRootDir/.openclaw";
17
+
18
+ if (!fs.existsSync(kRootResolved)) {
19
+ fs.mkdirSync(kRootResolved, { recursive: true });
20
+ }
21
+
22
+ const normalizeRelativePath = (inputPath) => {
23
+ const rawPath = String(inputPath || "").trim();
24
+ if (!rawPath) return "";
25
+ return rawPath.replace(/\\/g, "/").replace(/^\/+/, "");
26
+ };
27
+
28
+ const resolveSafePath = (inputPath) => {
29
+ const relativePath = normalizeRelativePath(inputPath);
30
+ const absolutePath = path.resolve(kRootResolved, relativePath);
31
+ const isInsideRoot =
32
+ absolutePath === kRootResolved || absolutePath.startsWith(kRootWithSep);
33
+ if (!isInsideRoot) {
34
+ return { ok: false, error: `Path must stay within ${kRootDisplayName}` };
35
+ }
36
+ return { ok: true, relativePath, absolutePath };
37
+ };
38
+
39
+ const isLikelyBinaryFile = (targetPath) => {
40
+ let fileHandle = null;
41
+ try {
42
+ fileHandle = fs.openSync(targetPath, "r");
43
+ const sample = Buffer.alloc(512);
44
+ const bytesRead = fs.readSync(fileHandle, sample, 0, sample.length, 0);
45
+ for (let index = 0; index < bytesRead; index += 1) {
46
+ if (sample[index] === 0) return true;
47
+ }
48
+ return false;
49
+ } finally {
50
+ if (fileHandle !== null) fs.closeSync(fileHandle);
51
+ }
52
+ };
53
+
54
+ const toRelativePath = (absolutePath) => {
55
+ const relative = path.relative(kRootResolved, absolutePath);
56
+ return relative === "" ? "" : relative.split(path.sep).join("/");
57
+ };
58
+
59
+ const buildTreeNode = (absolutePath, depthRemaining) => {
60
+ const stats = fs.statSync(absolutePath);
61
+ const nodeName = path.basename(absolutePath);
62
+ const nodePath = toRelativePath(absolutePath);
63
+
64
+ if (!stats.isDirectory()) {
65
+ return { type: "file", name: nodeName, path: nodePath };
66
+ }
67
+
68
+ if (depthRemaining <= 0) {
69
+ return { type: "folder", name: nodeName, path: nodePath, children: [] };
70
+ }
71
+
72
+ const children = fs
73
+ .readdirSync(absolutePath, { withFileTypes: true })
74
+ .filter((entry) => {
75
+ if (entry.isDirectory() && kIgnoredDirectoryNames.has(entry.name)) {
76
+ return false;
77
+ }
78
+ return entry.isDirectory() || entry.isFile();
79
+ })
80
+ .map((entry) => buildTreeNode(path.join(absolutePath, entry.name), depthRemaining - 1))
81
+ .sort((leftNode, rightNode) => {
82
+ if (leftNode.type !== rightNode.type) {
83
+ return leftNode.type === "folder" ? -1 : 1;
84
+ }
85
+ return leftNode.name.localeCompare(rightNode.name);
86
+ });
87
+
88
+ return { type: "folder", name: nodeName, path: nodePath, children };
89
+ };
90
+
91
+ const runGitSync = (message, relativeFilePath) =>
92
+ new Promise((resolve) => {
93
+ const syncArgs = ["git-sync", "-m", message];
94
+ if (relativeFilePath) {
95
+ syncArgs.push("--file", String(relativeFilePath));
96
+ }
97
+ execFile(
98
+ "alphaclaw",
99
+ syncArgs,
100
+ { timeout: 20000, cwd: kRootResolved },
101
+ (error) => {
102
+ if (error) {
103
+ return resolve({
104
+ ok: false,
105
+ error: error.message || "alphaclaw git-sync failed",
106
+ });
107
+ }
108
+ return resolve({ ok: true });
109
+ },
110
+ );
111
+ });
112
+
113
+ const runGitCommand = (args) =>
114
+ new Promise((resolve) => {
115
+ execFile(
116
+ "git",
117
+ args,
118
+ { timeout: 10000, cwd: kRootResolved },
119
+ (error, stdout, stderr) => {
120
+ if (error) {
121
+ return resolve({
122
+ ok: false,
123
+ error: String(stderr || stdout || error.message || "git command failed").trim(),
124
+ });
125
+ }
126
+ return resolve({ ok: true, stdout: String(stdout || "") });
127
+ },
128
+ );
129
+ });
130
+
131
+ const parseGithubRepoSlug = (value) => {
132
+ const raw = String(value || "").trim();
133
+ if (!raw) return "";
134
+ return raw
135
+ .replace(/^git@github\.com:/i, "")
136
+ .replace(/^https:\/\/github\.com\//i, "")
137
+ .replace(/\.git$/i, "")
138
+ .trim();
139
+ };
140
+
141
+ app.get("/api/browse/tree", (req, res) => {
142
+ const depthValue = Number.parseInt(String(req.query.depth || ""), 10);
143
+ const depth = Number.isFinite(depthValue) && depthValue > 0 ? depthValue : kDefaultTreeDepth;
144
+ try {
145
+ const tree = buildTreeNode(kRootResolved, depth);
146
+ return res.json({ ok: true, root: tree });
147
+ } catch (error) {
148
+ return res
149
+ .status(500)
150
+ .json({ ok: false, error: error.message || "Could not build file tree" });
151
+ }
152
+ });
153
+
154
+ app.get("/api/browse/read", (req, res) => {
155
+ const resolvedPath = resolveSafePath(req.query.path);
156
+ if (!resolvedPath.ok) {
157
+ return res.status(400).json({ ok: false, error: resolvedPath.error });
158
+ }
159
+
160
+ try {
161
+ const stats = fs.statSync(resolvedPath.absolutePath);
162
+ if (!stats.isFile()) {
163
+ return res.status(400).json({ ok: false, error: "Path is not a file" });
164
+ }
165
+ if (isLikelyBinaryFile(resolvedPath.absolutePath)) {
166
+ return res.status(400).json({ ok: false, error: "Binary files are not editable" });
167
+ }
168
+ const content = fs.readFileSync(resolvedPath.absolutePath, "utf8");
169
+ return res.json({
170
+ ok: true,
171
+ path: resolvedPath.relativePath,
172
+ content,
173
+ });
174
+ } catch (error) {
175
+ return res.status(500).json({ ok: false, error: error.message || "Could not read file" });
176
+ }
177
+ });
178
+
179
+ app.get("/api/browse/git-summary", async (req, res) => {
180
+ try {
181
+ const envRepoSlug = parseGithubRepoSlug(process.env.GITHUB_WORKSPACE_REPO || "");
182
+ const statusResult = await runGitCommand(["status", "--porcelain", "--branch"]);
183
+ if (!statusResult.ok) {
184
+ if (/not a git repository/i.test(statusResult.error || "")) {
185
+ return res.json({
186
+ ok: true,
187
+ isRepo: false,
188
+ repoPath: kRootResolved,
189
+ });
190
+ }
191
+ return res.status(500).json({
192
+ ok: false,
193
+ error: statusResult.error || "Could not read git status",
194
+ });
195
+ }
196
+
197
+ const statusLines = statusResult.stdout
198
+ .split("\n")
199
+ .map((line) => line.trimEnd())
200
+ .filter(Boolean);
201
+ const branchLine = statusLines.find((line) => line.startsWith("##")) || "";
202
+ const branch = branchLine.replace(/^##\s*/, "").split("...")[0] || "unknown";
203
+ const changedFiles = statusLines
204
+ .filter((line) => !line.startsWith("##"))
205
+ .map((line) => ({
206
+ status: line.slice(0, 2).trim() || "M",
207
+ path: line.slice(3).trim(),
208
+ }));
209
+
210
+ let repoSlug = envRepoSlug;
211
+ if (!repoSlug) {
212
+ const remoteResult = await runGitCommand(["remote", "get-url", "origin"]);
213
+ if (remoteResult.ok) {
214
+ repoSlug = parseGithubRepoSlug(remoteResult.stdout || "");
215
+ }
216
+ }
217
+ const repoUrl = repoSlug ? `https://github.com/${repoSlug}` : "";
218
+
219
+ const logResult = await runGitCommand([
220
+ "log",
221
+ "--pretty=format:%H%x09%h%x09%s%x09%ct",
222
+ "-n",
223
+ "5",
224
+ ]);
225
+ const commits = logResult.ok
226
+ ? logResult.stdout
227
+ .split("\n")
228
+ .map((line) => line.trim())
229
+ .filter(Boolean)
230
+ .map((line) => {
231
+ const [hash = "", shortHash = "", message = "", unixTs = "0"] = line.split("\t");
232
+ return {
233
+ hash,
234
+ shortHash,
235
+ message,
236
+ timestamp: Number.parseInt(unixTs, 10) || 0,
237
+ url: repoSlug && hash ? `${repoUrl}/commit/${hash}` : "",
238
+ };
239
+ })
240
+ : [];
241
+
242
+ return res.json({
243
+ ok: true,
244
+ isRepo: true,
245
+ repoPath: kRootResolved,
246
+ repoSlug,
247
+ repoUrl,
248
+ branch,
249
+ isDirty: changedFiles.length > 0,
250
+ changedFilesCount: changedFiles.length,
251
+ changedFiles: changedFiles.slice(0, 8),
252
+ commits,
253
+ });
254
+ } catch (error) {
255
+ return res.status(500).json({
256
+ ok: false,
257
+ error: error.message || "Could not build git summary",
258
+ });
259
+ }
260
+ });
261
+
262
+ app.put("/api/browse/write", async (req, res) => {
263
+ const { path: targetPath, content } = req.body || {};
264
+ const resolvedPath = resolveSafePath(targetPath);
265
+ if (!resolvedPath.ok) {
266
+ return res.status(400).json({ ok: false, error: resolvedPath.error });
267
+ }
268
+ if (typeof content !== "string") {
269
+ return res.status(400).json({ ok: false, error: "content must be a string" });
270
+ }
271
+
272
+ try {
273
+ const stats = fs.statSync(resolvedPath.absolutePath);
274
+ if (!stats.isFile()) {
275
+ return res.status(400).json({ ok: false, error: "Path is not a file" });
276
+ }
277
+ fs.writeFileSync(resolvedPath.absolutePath, content, "utf8");
278
+ const fileName = path.basename(resolvedPath.absolutePath);
279
+ const syncResult = await runGitSync(
280
+ `Edit ${fileName} via UI`,
281
+ resolvedPath.relativePath,
282
+ );
283
+ return res.json({
284
+ ok: true,
285
+ path: resolvedPath.relativePath,
286
+ synced: syncResult.ok,
287
+ syncError: syncResult.ok ? undefined : syncResult.error,
288
+ });
289
+ } catch (error) {
290
+ return res.status(500).json({ ok: false, error: error.message || "Could not save file" });
291
+ }
292
+ });
293
+ };
294
+
295
+ module.exports = { registerBrowseRoutes };
package/lib/server.js CHANGED
@@ -65,7 +65,10 @@ const { createAlphaclawVersionService } = require("./server/alphaclaw-version");
65
65
  const {
66
66
  createRestartRequiredState,
67
67
  } = require("./server/restart-required-state");
68
- const { syncBootstrapPromptFiles } = require("./server/onboarding/workspace");
68
+ const {
69
+ installControlUiSkill,
70
+ syncBootstrapPromptFiles,
71
+ } = require("./server/onboarding/workspace");
69
72
  const { createTelegramApi } = require("./server/telegram-api");
70
73
  const { createDiscordApi } = require("./server/discord-api");
71
74
  const { createWatchdogNotifier } = require("./server/watchdog-notify");
@@ -79,6 +82,7 @@ const { registerSystemRoutes } = require("./server/routes/system");
79
82
  const { registerPairingRoutes } = require("./server/routes/pairings");
80
83
  const { registerCodexRoutes } = require("./server/routes/codex");
81
84
  const { registerGoogleRoutes } = require("./server/routes/google");
85
+ const { registerBrowseRoutes } = require("./server/routes/browse");
82
86
  const { registerProxyRoutes } = require("./server/routes/proxy");
83
87
  const { registerTelegramRoutes } = require("./server/routes/telegram");
84
88
  const { registerWebhookRoutes } = require("./server/routes/webhooks");
@@ -196,6 +200,11 @@ registerSystemRoutes({
196
200
  OPENCLAW_DIR: constants.OPENCLAW_DIR,
197
201
  restartRequiredState,
198
202
  });
203
+ registerBrowseRoutes({
204
+ app,
205
+ fs,
206
+ kRootDir: constants.OPENCLAW_DIR,
207
+ });
199
208
  registerPairingRoutes({ app, clawCmd, isOnboarded });
200
209
  registerCodexRoutes({
201
210
  app,
@@ -229,8 +238,20 @@ const watchdog = createWatchdog({
229
238
  });
230
239
  setGatewayExitHandler((payload) => watchdog.onGatewayExit(payload));
231
240
  setGatewayLaunchHandler((payload) => watchdog.onGatewayLaunch(payload));
232
- const doSyncPromptFiles = () =>
233
- syncBootstrapPromptFiles({ fs, workspaceDir: constants.WORKSPACE_DIR });
241
+ const doSyncPromptFiles = () => {
242
+ const setupUiUrl = resolveSetupUrl();
243
+ syncBootstrapPromptFiles({
244
+ fs,
245
+ workspaceDir: constants.WORKSPACE_DIR,
246
+ baseUrl: setupUiUrl,
247
+ });
248
+ installControlUiSkill({
249
+ fs,
250
+ openclawDir: constants.OPENCLAW_DIR,
251
+ baseUrl: setupUiUrl,
252
+ });
253
+ };
254
+ doSyncPromptFiles();
234
255
  registerTelegramRoutes({
235
256
  app,
236
257
  telegramApi,
@@ -9,9 +9,11 @@ AlphaClaw UI: `{{SETUP_UI_URL}}`
9
9
  | Tab | URL | What it manages |
10
10
  | --------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
11
11
  | General | `{{SETUP_UI_URL}}#general` | Gateway status & restart, channel health (Telegram/Discord), pending pairings, feature health (Embeddings/Audio), Google Workspace connection, repo auto-sync schedule, OpenClaw dashboard |
12
+ | Watchdog | `{{SETUP_UI_URL}}#watchdog` | Gateway watchdog lifecycle, crash-loop visibility, restart diagnostics, and auto-repair feature |
12
13
  | Providers | `{{SETUP_UI_URL}}#providers` | AI provider credentials (Anthropic, OpenAI, Gemini, Mistral, Voyage, Groq, Deepgram), feature capabilities, Codex OAuth |
13
14
  | Envars | `{{SETUP_UI_URL}}#envars` | View/edit/add environment variables (saved to `/data/.env`), gateway restart to apply changes |
14
15
  | Webhooks | `{{SETUP_UI_URL}}#webhooks` | Webhook endpoint visibility, create flow, request history, and gateway delivery debugging |
16
+ | Browse | `{{SETUP_UI_URL}}#browse` | File browser and editor rooted at `.openclaw`, markdown preview/edit flow, and git-aware save workflow |
15
17
 
16
18
  ### Environment variables
17
19
 
@@ -38,7 +40,7 @@ After pushing, include a link to the commit using the abbreviated hash: [abc1234
38
40
 
39
41
  ## Webhooks
40
42
 
41
- You can create webhooks yourself or the user can create them through the Setup UI.
43
+ You can create webhooks yourself or the user can create them through the AlphaClaw UI.
42
44
 
43
45
  Webhook transform files must follow this convention:
44
46
 
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: control-ui
3
- description: Know when and how to direct the user to the Setup UI for configuration tasks.
2
+ name: alphaclaw
3
+ description: Know when and how to direct the user to the AlphaClaw UI for configuration tasks.
4
4
  ---
5
5
 
6
- # Control UI
6
+ # AlphaClaw UI
7
7
 
8
8
  There is a web-based Setup UI at `{{BASE_URL}}`. The **user** manages runtime configuration through it. You should NOT call these API endpoints yourself or write config files directly — instead, tell the user what they need to do and where to do it.
9
9
 
@@ -35,16 +35,16 @@ When a user asks about pairing their Telegram or Discord account:
35
35
 
36
36
  Supported Google services (user selects which to enable during OAuth):
37
37
 
38
- | Service | Read | Write | Google API |
39
- |---------|------|-------|------------|
40
- | Gmail | `gmail:read` | `gmail:write` | `gmail.googleapis.com` |
38
+ | Service | Read | Write | Google API |
39
+ | -------- | --------------- | ---------------- | ------------------------------ |
40
+ | Gmail | `gmail:read` | `gmail:write` | `gmail.googleapis.com` |
41
41
  | Calendar | `calendar:read` | `calendar:write` | `calendar-json.googleapis.com` |
42
- | Drive | `drive:read` | `drive:write` | `drive.googleapis.com` |
43
- | Sheets | `sheets:read` | `sheets:write` | `sheets.googleapis.com` |
44
- | Docs | `docs:read` | `docs:write` | `docs.googleapis.com` |
45
- | Tasks | `tasks:read` | `tasks:write` | `tasks.googleapis.com` |
46
- | Contacts | `contacts:read` | `contacts:write` | `people.googleapis.com` |
47
- | Meet | `meet:read` | `meet:write` | `meet.googleapis.com` |
42
+ | Drive | `drive:read` | `drive:write` | `drive.googleapis.com` |
43
+ | Sheets | `sheets:read` | `sheets:write` | `sheets.googleapis.com` |
44
+ | Docs | `docs:read` | `docs:write` | `docs.googleapis.com` |
45
+ | Tasks | `tasks:read` | `tasks:write` | `tasks.googleapis.com` |
46
+ | Contacts | `contacts:read` | `contacts:write` | `people.googleapis.com` |
47
+ | Meet | `meet:read` | `meet:write` | `meet.googleapis.com` |
48
48
 
49
49
  Default enabled: Gmail (read), Calendar (read+write), Drive (read), Sheets (read), Docs (read).
50
50
 
@@ -60,11 +60,3 @@ gog contacts list --account user@gmail.com
60
60
  ```
61
61
 
62
62
  Config lives at `/data/.openclaw/gogcli/`.
63
-
64
- ## What the Setup UI provides (for your awareness)
65
-
66
- This is a reference so you know what's available — not an invitation to call these endpoints.
67
-
68
- - **General tab** (`{{BASE_URL}}#general`): Gateway status/restart, OpenClaw version + update, channel health, pending pairings, feature health (Embeddings/Audio), Google Workspace
69
- - **Providers tab** (`{{BASE_URL}}#providers`): Primary model selection, AI provider credentials, feature capabilities, Codex OAuth
70
- - **Envars tab** (`{{BASE_URL}}#envars`): View/edit/add environment variables, save to `/data/.env`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },