@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.
- package/bin/alphaclaw.js +29 -2
- package/lib/cli/git-sync.js +25 -0
- package/lib/public/css/explorer.css +983 -0
- package/lib/public/css/shell.css +48 -4
- package/lib/public/css/theme.css +6 -1
- package/lib/public/icons/folder-line.svg +1 -0
- package/lib/public/icons/hashtag.svg +3 -0
- package/lib/public/icons/home-5-line.svg +1 -0
- package/lib/public/icons/save-fill.svg +3 -0
- package/lib/public/js/app.js +259 -158
- package/lib/public/js/components/action-button.js +12 -1
- package/lib/public/js/components/file-tree.js +322 -0
- package/lib/public/js/components/file-viewer.js +691 -0
- package/lib/public/js/components/icons.js +182 -0
- package/lib/public/js/components/sidebar-git-panel.js +149 -0
- package/lib/public/js/components/sidebar.js +272 -0
- package/lib/public/js/lib/api.js +26 -0
- package/lib/public/js/lib/browse-draft-state.js +109 -0
- package/lib/public/js/lib/file-highlighting.js +6 -0
- package/lib/public/js/lib/file-tree-utils.js +12 -0
- package/lib/public/js/lib/syntax-highlighters/css.js +124 -0
- package/lib/public/js/lib/syntax-highlighters/frontmatter.js +49 -0
- package/lib/public/js/lib/syntax-highlighters/html.js +209 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +28 -0
- package/lib/public/js/lib/syntax-highlighters/javascript.js +134 -0
- package/lib/public/js/lib/syntax-highlighters/json.js +61 -0
- package/lib/public/js/lib/syntax-highlighters/markdown.js +37 -0
- package/lib/public/js/lib/syntax-highlighters/utils.js +13 -0
- package/lib/public/setup.html +1 -0
- package/lib/server/constants.js +1 -0
- package/lib/server/onboarding/workspace.js +3 -2
- package/lib/server/routes/browse.js +295 -0
- package/lib/server.js +24 -3
- package/lib/setup/core-prompts/TOOLS.md +3 -1
- package/lib/setup/skills/control-ui/SKILL.md +12 -20
- 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("&", "&")
|
|
4
|
+
.replaceAll("<", "<")
|
|
5
|
+
.replaceAll(">", ">");
|
|
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
|
+
}));
|
package/lib/public/setup.html
CHANGED
|
@@ -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 = {
|
package/lib/server/constants.js
CHANGED
|
@@ -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,
|
|
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 (${
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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:
|
|
3
|
-
description: Know when and how to direct the user to the
|
|
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
|
-
#
|
|
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
|
|
39
|
-
|
|
40
|
-
| Gmail
|
|
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
|
|
43
|
-
| Sheets
|
|
44
|
-
| Docs
|
|
45
|
-
| Tasks
|
|
46
|
-
| Contacts | `contacts:read` | `contacts:write` | `people.googleapis.com`
|
|
47
|
-
| Meet
|
|
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`
|