@chrysb/alphaclaw 0.3.4-beta.0 → 0.3.5-beta.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.
- package/bin/alphaclaw.js +17 -2
- package/lib/public/css/explorer.css +184 -3
- package/lib/public/css/theme.css +1 -1
- package/lib/public/js/app.js +57 -7
- package/lib/public/js/components/file-tree.js +100 -25
- package/lib/public/js/components/file-viewer.js +543 -162
- package/lib/public/js/components/gateway.js +12 -18
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/sidebar-git-panel.js +155 -28
- package/lib/public/js/components/sidebar.js +1 -1
- package/lib/public/js/components/watchdog-tab.js +0 -2
- package/lib/public/js/lib/api.js +15 -0
- package/lib/server/helpers.js +18 -5
- package/lib/server/internal-files-migration.js +93 -0
- package/lib/server/onboarding/cron.js +6 -4
- package/lib/server/onboarding/index.js +7 -0
- package/lib/server/onboarding/openclaw.js +6 -1
- package/lib/server/routes/browse.js +233 -28
- package/lib/server/routes/pairings.js +8 -2
- package/lib/server/routes/system.js +5 -1
- package/lib/server.js +7 -0
- package/package.json +1 -1
|
@@ -4,11 +4,32 @@ const { execFile } = require("child_process");
|
|
|
4
4
|
const kDefaultTreeDepth = 10;
|
|
5
5
|
const kIgnoredDirectoryNames = new Set([
|
|
6
6
|
".git",
|
|
7
|
+
".alphaclaw",
|
|
7
8
|
"node_modules",
|
|
8
9
|
".cache",
|
|
9
10
|
"dist",
|
|
10
11
|
"build",
|
|
11
12
|
]);
|
|
13
|
+
const kLockedBrowsePaths = new Set([
|
|
14
|
+
"hooks/bootstrap/agents.md",
|
|
15
|
+
"hooks/bootstrap/tools.md",
|
|
16
|
+
".alphaclaw/hourly-git-sync.sh",
|
|
17
|
+
".alphaclaw/.cli-device-auto-approved",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const matchesPolicyPath = (policyPathSet, normalizedPath) => {
|
|
21
|
+
const safeNormalizedPath = String(normalizedPath || "").trim();
|
|
22
|
+
if (!safeNormalizedPath) return false;
|
|
23
|
+
for (const policyPath of policyPathSet) {
|
|
24
|
+
if (
|
|
25
|
+
safeNormalizedPath === policyPath ||
|
|
26
|
+
safeNormalizedPath.endsWith(`/${policyPath}`)
|
|
27
|
+
) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
};
|
|
12
33
|
|
|
13
34
|
const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
14
35
|
const kRootResolved = path.resolve(kRootDir);
|
|
@@ -25,6 +46,14 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
25
46
|
return rawPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
26
47
|
};
|
|
27
48
|
|
|
49
|
+
const normalizePolicyPath = (inputPath) =>
|
|
50
|
+
String(inputPath || "")
|
|
51
|
+
.replace(/\\/g, "/")
|
|
52
|
+
.replace(/^\.\/+/, "")
|
|
53
|
+
.replace(/^\/+/, "")
|
|
54
|
+
.trim()
|
|
55
|
+
.toLowerCase();
|
|
56
|
+
|
|
28
57
|
const resolveSafePath = (inputPath) => {
|
|
29
58
|
const relativePath = normalizeRelativePath(inputPath);
|
|
30
59
|
const absolutePath = path.resolve(kRootResolved, relativePath);
|
|
@@ -88,42 +117,48 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
88
117
|
return { type: "folder", name: nodeName, path: nodePath, children };
|
|
89
118
|
};
|
|
90
119
|
|
|
91
|
-
const
|
|
120
|
+
const runGitCommand = (args) =>
|
|
92
121
|
new Promise((resolve) => {
|
|
93
|
-
const syncArgs = ["git-sync", "-m", message];
|
|
94
|
-
if (relativeFilePath) {
|
|
95
|
-
syncArgs.push("--file", String(relativeFilePath));
|
|
96
|
-
}
|
|
97
122
|
execFile(
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
{ timeout:
|
|
101
|
-
(error) => {
|
|
123
|
+
"git",
|
|
124
|
+
args,
|
|
125
|
+
{ timeout: 10000, cwd: kRootResolved },
|
|
126
|
+
(error, stdout, stderr) => {
|
|
102
127
|
if (error) {
|
|
103
128
|
return resolve({
|
|
104
129
|
ok: false,
|
|
105
|
-
error: error.message || "
|
|
130
|
+
error: String(stderr || stdout || error.message || "git command failed").trim(),
|
|
106
131
|
});
|
|
107
132
|
}
|
|
108
|
-
return resolve({ ok: true });
|
|
133
|
+
return resolve({ ok: true, stdout: String(stdout || "") });
|
|
109
134
|
},
|
|
110
135
|
);
|
|
111
136
|
});
|
|
112
137
|
|
|
113
|
-
const
|
|
138
|
+
const runGitCommandWithExitCode = (args) =>
|
|
114
139
|
new Promise((resolve) => {
|
|
115
140
|
execFile(
|
|
116
141
|
"git",
|
|
117
142
|
args,
|
|
118
143
|
{ timeout: 10000, cwd: kRootResolved },
|
|
119
144
|
(error, stdout, stderr) => {
|
|
120
|
-
|
|
145
|
+
const safeStdout = String(stdout || "");
|
|
146
|
+
const safeStderr = String(stderr || "");
|
|
147
|
+
if (!error) {
|
|
121
148
|
return resolve({
|
|
122
|
-
ok:
|
|
123
|
-
|
|
149
|
+
ok: true,
|
|
150
|
+
stdout: safeStdout,
|
|
151
|
+
stderr: safeStderr,
|
|
152
|
+
exitCode: 0,
|
|
124
153
|
});
|
|
125
154
|
}
|
|
126
|
-
return resolve({
|
|
155
|
+
return resolve({
|
|
156
|
+
ok: false,
|
|
157
|
+
stdout: safeStdout,
|
|
158
|
+
stderr: safeStderr,
|
|
159
|
+
exitCode: Number.isInteger(error.code) ? error.code : 1,
|
|
160
|
+
error: String(error.message || "git command failed"),
|
|
161
|
+
});
|
|
127
162
|
},
|
|
128
163
|
);
|
|
129
164
|
});
|
|
@@ -138,6 +173,16 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
138
173
|
.trim();
|
|
139
174
|
};
|
|
140
175
|
|
|
176
|
+
const normalizeChangedPath = (rawPath) => {
|
|
177
|
+
const value = String(rawPath || "").trim();
|
|
178
|
+
if (!value) return "";
|
|
179
|
+
if (value.includes(" -> ")) {
|
|
180
|
+
const segments = value.split(" -> ");
|
|
181
|
+
return String(segments[segments.length - 1] || "").trim();
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
};
|
|
185
|
+
|
|
141
186
|
app.get("/api/browse/tree", (req, res) => {
|
|
142
187
|
const depthValue = Number.parseInt(String(req.query.depth || ""), 10);
|
|
143
188
|
const depth = Number.isFinite(depthValue) && depthValue > 0 ? depthValue : kDefaultTreeDepth;
|
|
@@ -200,12 +245,48 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
200
245
|
.filter(Boolean);
|
|
201
246
|
const branchLine = statusLines.find((line) => line.startsWith("##")) || "";
|
|
202
247
|
const branch = branchLine.replace(/^##\s*/, "").split("...")[0] || "unknown";
|
|
248
|
+
const diffNumstatResult = await runGitCommand(["diff", "--numstat", "HEAD"]);
|
|
249
|
+
const diffStatsByPath = new Map();
|
|
250
|
+
if (diffNumstatResult.ok) {
|
|
251
|
+
diffNumstatResult.stdout
|
|
252
|
+
.split("\n")
|
|
253
|
+
.map((line) => line.trim())
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
.forEach((line) => {
|
|
256
|
+
const [addedRaw = "", deletedRaw = "", rawPath = ""] = line.split("\t");
|
|
257
|
+
const normalizedPath = normalizeChangedPath(rawPath);
|
|
258
|
+
if (!normalizedPath) return;
|
|
259
|
+
const addedLines = Number.parseInt(addedRaw, 10);
|
|
260
|
+
const deletedLines = Number.parseInt(deletedRaw, 10);
|
|
261
|
+
diffStatsByPath.set(normalizedPath, {
|
|
262
|
+
addedLines: Number.isFinite(addedLines) ? addedLines : null,
|
|
263
|
+
deletedLines: Number.isFinite(deletedLines) ? deletedLines : null,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
203
267
|
const changedFiles = statusLines
|
|
204
268
|
.filter((line) => !line.startsWith("##"))
|
|
205
|
-
.map((line) =>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
269
|
+
.map((line) => {
|
|
270
|
+
const rawStatus = line.slice(0, 2);
|
|
271
|
+
const pathValue = normalizeChangedPath(line.slice(3));
|
|
272
|
+
const stats = diffStatsByPath.get(pathValue) || {
|
|
273
|
+
addedLines: null,
|
|
274
|
+
deletedLines: null,
|
|
275
|
+
};
|
|
276
|
+
const statusKind =
|
|
277
|
+
rawStatus === "??" || rawStatus.includes("A")
|
|
278
|
+
? "U"
|
|
279
|
+
: rawStatus.includes("D")
|
|
280
|
+
? "D"
|
|
281
|
+
: "M";
|
|
282
|
+
return {
|
|
283
|
+
status: rawStatus.trim() || "M",
|
|
284
|
+
statusKind,
|
|
285
|
+
path: pathValue,
|
|
286
|
+
addedLines: stats.addedLines,
|
|
287
|
+
deletedLines: stats.deletedLines,
|
|
288
|
+
};
|
|
289
|
+
});
|
|
209
290
|
|
|
210
291
|
let repoSlug = envRepoSlug;
|
|
211
292
|
if (!repoSlug) {
|
|
@@ -248,7 +329,7 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
248
329
|
branch,
|
|
249
330
|
isDirty: changedFiles.length > 0,
|
|
250
331
|
changedFilesCount: changedFiles.length,
|
|
251
|
-
changedFiles: changedFiles.slice(0,
|
|
332
|
+
changedFiles: changedFiles.slice(0, 16),
|
|
252
333
|
commits,
|
|
253
334
|
});
|
|
254
335
|
} catch (error) {
|
|
@@ -259,12 +340,143 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
259
340
|
}
|
|
260
341
|
});
|
|
261
342
|
|
|
343
|
+
app.get("/api/browse/git-diff", async (req, res) => {
|
|
344
|
+
const resolvedPath = resolveSafePath(req.query.path);
|
|
345
|
+
if (!resolvedPath.ok) {
|
|
346
|
+
return res.status(400).json({ ok: false, error: resolvedPath.error });
|
|
347
|
+
}
|
|
348
|
+
const relativePath = String(resolvedPath.relativePath || "").trim();
|
|
349
|
+
if (!relativePath) {
|
|
350
|
+
return res.status(400).json({ ok: false, error: "path is required" });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const statusResult = await runGitCommandWithExitCode([
|
|
355
|
+
"status",
|
|
356
|
+
"--porcelain",
|
|
357
|
+
"--",
|
|
358
|
+
relativePath,
|
|
359
|
+
]);
|
|
360
|
+
if (!statusResult.ok && /not a git repository/i.test(statusResult.stderr || "")) {
|
|
361
|
+
return res.status(400).json({ ok: false, error: "No git repo at this root" });
|
|
362
|
+
}
|
|
363
|
+
const statusLines = statusResult.stdout
|
|
364
|
+
.split("\n")
|
|
365
|
+
.map((line) => line.trim())
|
|
366
|
+
.filter(Boolean);
|
|
367
|
+
const isUntracked = statusLines.some((line) => line.startsWith("??"));
|
|
368
|
+
|
|
369
|
+
const diffResult = isUntracked
|
|
370
|
+
? await runGitCommandWithExitCode([
|
|
371
|
+
"diff",
|
|
372
|
+
"--no-index",
|
|
373
|
+
"--",
|
|
374
|
+
"/dev/null",
|
|
375
|
+
resolvedPath.absolutePath,
|
|
376
|
+
])
|
|
377
|
+
: await runGitCommandWithExitCode(["diff", "HEAD", "--", relativePath]);
|
|
378
|
+
|
|
379
|
+
const untrackedAllowedFailure =
|
|
380
|
+
isUntracked && diffResult.exitCode === 1 && diffResult.stdout;
|
|
381
|
+
if (!diffResult.ok && !untrackedAllowedFailure) {
|
|
382
|
+
return res.status(500).json({
|
|
383
|
+
ok: false,
|
|
384
|
+
error: diffResult.stderr || diffResult.error || "Could not load file diff",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const content = String(diffResult.stdout || "")
|
|
389
|
+
.replaceAll(resolvedPath.absolutePath, relativePath)
|
|
390
|
+
.trimEnd();
|
|
391
|
+
return res.json({
|
|
392
|
+
ok: true,
|
|
393
|
+
path: relativePath,
|
|
394
|
+
content,
|
|
395
|
+
});
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return res
|
|
398
|
+
.status(500)
|
|
399
|
+
.json({ ok: false, error: error.message || "Could not load file diff" });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
app.post("/api/browse/git-sync", async (req, res) => {
|
|
404
|
+
try {
|
|
405
|
+
const commitMessageRaw = String(req.body?.message || "").trim();
|
|
406
|
+
const commitMessage = commitMessageRaw || "sync changes";
|
|
407
|
+
const statusResult = await runGitCommand(["status", "--porcelain"]);
|
|
408
|
+
if (!statusResult.ok) {
|
|
409
|
+
if (/not a git repository/i.test(statusResult.error || "")) {
|
|
410
|
+
return res
|
|
411
|
+
.status(400)
|
|
412
|
+
.json({ ok: false, error: "No git repo at this root" });
|
|
413
|
+
}
|
|
414
|
+
return res.status(500).json({
|
|
415
|
+
ok: false,
|
|
416
|
+
error: statusResult.error || "Could not read git status",
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const hasChanges = statusResult.stdout
|
|
420
|
+
.split("\n")
|
|
421
|
+
.map((line) => line.trim())
|
|
422
|
+
.filter(Boolean).length > 0;
|
|
423
|
+
if (!hasChanges) {
|
|
424
|
+
return res.json({
|
|
425
|
+
ok: true,
|
|
426
|
+
committed: false,
|
|
427
|
+
message: "No changes to sync",
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const addResult = await runGitCommand(["add", "-A"]);
|
|
431
|
+
if (!addResult.ok) {
|
|
432
|
+
return res.status(500).json({
|
|
433
|
+
ok: false,
|
|
434
|
+
error: addResult.error || "Could not stage changes",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const commitResult = await runGitCommand(["commit", "-m", commitMessage]);
|
|
438
|
+
if (!commitResult.ok) {
|
|
439
|
+
if (/nothing to commit/i.test(commitResult.error || "")) {
|
|
440
|
+
return res.json({
|
|
441
|
+
ok: true,
|
|
442
|
+
committed: false,
|
|
443
|
+
message: "No changes to sync",
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return res.status(500).json({
|
|
447
|
+
ok: false,
|
|
448
|
+
error: commitResult.error || "Could not commit changes",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const shortHashResult = await runGitCommand(["rev-parse", "--short", "HEAD"]);
|
|
452
|
+
const shortHash = shortHashResult.ok ? String(shortHashResult.stdout || "").trim() : "";
|
|
453
|
+
return res.json({
|
|
454
|
+
ok: true,
|
|
455
|
+
committed: true,
|
|
456
|
+
shortHash,
|
|
457
|
+
message: `Committed ${shortHash || "changes"}`,
|
|
458
|
+
});
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return res.status(500).json({
|
|
461
|
+
ok: false,
|
|
462
|
+
error: error.message || "Could not sync changes",
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
262
467
|
app.put("/api/browse/write", async (req, res) => {
|
|
263
468
|
const { path: targetPath, content } = req.body || {};
|
|
264
469
|
const resolvedPath = resolveSafePath(targetPath);
|
|
265
470
|
if (!resolvedPath.ok) {
|
|
266
471
|
return res.status(400).json({ ok: false, error: resolvedPath.error });
|
|
267
472
|
}
|
|
473
|
+
const normalizedPolicyPath = normalizePolicyPath(resolvedPath.relativePath);
|
|
474
|
+
if (matchesPolicyPath(kLockedBrowsePaths, normalizedPolicyPath)) {
|
|
475
|
+
return res.status(403).json({
|
|
476
|
+
ok: false,
|
|
477
|
+
error: "This file is managed by Alpha Claw and cannot be edited.",
|
|
478
|
+
});
|
|
479
|
+
}
|
|
268
480
|
if (typeof content !== "string") {
|
|
269
481
|
return res.status(400).json({ ok: false, error: "content must be a string" });
|
|
270
482
|
}
|
|
@@ -275,16 +487,9 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
275
487
|
return res.status(400).json({ ok: false, error: "Path is not a file" });
|
|
276
488
|
}
|
|
277
489
|
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
490
|
return res.json({
|
|
284
491
|
ok: true,
|
|
285
492
|
path: resolvedPath.relativePath,
|
|
286
|
-
synced: syncResult.ok,
|
|
287
|
-
syncError: syncResult.ok ? undefined : syncResult.error,
|
|
288
493
|
});
|
|
289
494
|
} catch (error) {
|
|
290
495
|
return res.status(500).json({ ok: false, error: error.message || "Could not save file" });
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const { OPENCLAW_DIR } = require("../constants");
|
|
3
|
+
const { buildManagedPaths } = require("../internal-files-migration");
|
|
3
4
|
|
|
4
5
|
const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openclawDir = OPENCLAW_DIR }) => {
|
|
5
6
|
let pairingCache = { pending: [], ts: 0 };
|
|
6
7
|
const PAIRING_CACHE_TTL = 10000;
|
|
7
|
-
const
|
|
8
|
+
const {
|
|
9
|
+
cliDeviceAutoApprovedPath: kCliAutoApproveMarkerPath,
|
|
10
|
+
internalDir: kManagedFilesDir,
|
|
11
|
+
} = buildManagedPaths({
|
|
12
|
+
openclawDir,
|
|
13
|
+
});
|
|
8
14
|
|
|
9
15
|
const hasCliAutoApproveMarker = () => fsModule.existsSync(kCliAutoApproveMarkerPath);
|
|
10
16
|
|
|
11
17
|
const writeCliAutoApproveMarker = () => {
|
|
12
|
-
fsModule.mkdirSync(
|
|
18
|
+
fsModule.mkdirSync(kManagedFilesDir, { recursive: true });
|
|
13
19
|
fsModule.writeFileSync(
|
|
14
20
|
kCliAutoApproveMarkerPath,
|
|
15
21
|
JSON.stringify({ approvedAt: new Date().toISOString() }, null, 2),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { buildManagedPaths } = require("../internal-files-migration");
|
|
2
|
+
|
|
1
3
|
const registerSystemRoutes = ({
|
|
2
4
|
app,
|
|
3
5
|
fs,
|
|
@@ -35,7 +37,9 @@ const registerSystemRoutes = ({
|
|
|
35
37
|
kSystemVars.has(key) || kEnvVarsReservedForUserInput.has(key);
|
|
36
38
|
const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
|
|
37
39
|
const kSystemCronConfigPath = `${OPENCLAW_DIR}/cron/system-sync.json`;
|
|
38
|
-
const kSystemCronScriptPath =
|
|
40
|
+
const { hourlyGitSyncPath: kSystemCronScriptPath } = buildManagedPaths({
|
|
41
|
+
openclawDir: OPENCLAW_DIR,
|
|
42
|
+
});
|
|
39
43
|
const kDefaultSystemCronSchedule = "0 * * * *";
|
|
40
44
|
const isValidCronSchedule = (value) =>
|
|
41
45
|
typeof value === "string" && /^(\S+\s+){4}\S+$/.test(value.trim());
|
package/lib/server.js
CHANGED
|
@@ -76,6 +76,9 @@ const {
|
|
|
76
76
|
installControlUiSkill,
|
|
77
77
|
syncBootstrapPromptFiles,
|
|
78
78
|
} = require("./server/onboarding/workspace");
|
|
79
|
+
const {
|
|
80
|
+
migrateManagedInternalFiles,
|
|
81
|
+
} = require("./server/internal-files-migration");
|
|
79
82
|
const { createTelegramApi } = require("./server/telegram-api");
|
|
80
83
|
const { createDiscordApi } = require("./server/discord-api");
|
|
81
84
|
const { createWatchdogNotifier } = require("./server/watchdog-notify");
|
|
@@ -100,6 +103,10 @@ const { PORT, GATEWAY_URL, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
|
|
|
100
103
|
|
|
101
104
|
startEnvWatcher();
|
|
102
105
|
attachGatewaySignalHandlers();
|
|
106
|
+
migrateManagedInternalFiles({
|
|
107
|
+
fs,
|
|
108
|
+
openclawDir: constants.OPENCLAW_DIR,
|
|
109
|
+
});
|
|
103
110
|
|
|
104
111
|
const app = express();
|
|
105
112
|
app.set("trust proxy", kTrustProxyHops);
|