@chrysb/alphaclaw 0.3.4 → 0.3.5-beta.1

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 (55) hide show
  1. package/bin/alphaclaw.js +82 -3
  2. package/lib/public/css/explorer.css +385 -9
  3. package/lib/public/css/theme.css +1 -1
  4. package/lib/public/js/app.js +102 -8
  5. package/lib/public/js/components/channels.js +1 -0
  6. package/lib/public/js/components/file-tree.js +74 -38
  7. package/lib/public/js/components/file-viewer/constants.js +6 -0
  8. package/lib/public/js/components/file-viewer/diff-viewer.js +46 -0
  9. package/lib/public/js/components/file-viewer/editor-surface.js +120 -0
  10. package/lib/public/js/components/file-viewer/frontmatter-panel.js +56 -0
  11. package/lib/public/js/components/file-viewer/index.js +164 -0
  12. package/lib/public/js/components/file-viewer/markdown-split-view.js +51 -0
  13. package/lib/public/js/components/file-viewer/media-preview.js +44 -0
  14. package/lib/public/js/components/file-viewer/scroll-sync.js +95 -0
  15. package/lib/public/js/components/file-viewer/sqlite-viewer.js +167 -0
  16. package/lib/public/js/components/file-viewer/status-banners.js +59 -0
  17. package/lib/public/js/components/file-viewer/storage.js +58 -0
  18. package/lib/public/js/components/file-viewer/toolbar.js +77 -0
  19. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +87 -0
  20. package/lib/public/js/components/file-viewer/use-file-diff.js +49 -0
  21. package/lib/public/js/components/file-viewer/use-file-loader.js +302 -0
  22. package/lib/public/js/components/file-viewer/use-file-viewer-draft-sync.js +32 -0
  23. package/lib/public/js/components/file-viewer/use-file-viewer-hotkeys.js +25 -0
  24. package/lib/public/js/components/file-viewer/use-file-viewer.js +379 -0
  25. package/lib/public/js/components/file-viewer/utils.js +11 -0
  26. package/lib/public/js/components/gateway.js +95 -48
  27. package/lib/public/js/components/icons.js +26 -0
  28. package/lib/public/js/components/sidebar-git-panel.js +219 -31
  29. package/lib/public/js/components/sidebar.js +1 -1
  30. package/lib/public/js/components/usage-tab.js +4 -1
  31. package/lib/public/js/components/watchdog-tab.js +6 -2
  32. package/lib/public/js/lib/api.js +31 -0
  33. package/lib/public/js/lib/browse-file-policies.js +34 -0
  34. package/lib/scripts/git +40 -0
  35. package/lib/scripts/git-askpass +6 -0
  36. package/lib/server/constants.js +8 -0
  37. package/lib/server/helpers.js +18 -5
  38. package/lib/server/internal-files-migration.js +93 -0
  39. package/lib/server/onboarding/cron.js +6 -4
  40. package/lib/server/onboarding/index.js +7 -0
  41. package/lib/server/onboarding/openclaw.js +6 -1
  42. package/lib/server/routes/browse/constants.js +51 -0
  43. package/lib/server/routes/browse/file-helpers.js +43 -0
  44. package/lib/server/routes/browse/git.js +131 -0
  45. package/lib/server/routes/browse/index.js +572 -0
  46. package/lib/server/routes/browse/path-utils.js +53 -0
  47. package/lib/server/routes/browse/sqlite.js +140 -0
  48. package/lib/server/routes/pairings.js +8 -2
  49. package/lib/server/routes/proxy.js +11 -5
  50. package/lib/server/routes/system.js +5 -1
  51. package/lib/server.js +7 -0
  52. package/lib/setup/core-prompts/TOOLS.md +0 -4
  53. package/package.json +1 -1
  54. package/lib/public/js/components/file-viewer.js +0 -864
  55. package/lib/server/routes/browse.js +0 -295
@@ -1,295 +0,0 @@
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 };