@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 CHANGED
@@ -10,6 +10,9 @@ const {
10
10
  validateGitSyncFilePath,
11
11
  } = require("../lib/cli/git-sync");
12
12
  const { buildSecretReplacements } = require("../lib/server/helpers");
13
+ const {
14
+ migrateManagedInternalFiles,
15
+ } = require("../lib/server/internal-files-migration");
13
16
 
14
17
  const kUsageTrackerPluginPath = path.resolve(
15
18
  __dirname,
@@ -143,6 +146,10 @@ if (portFlag) {
143
146
 
144
147
  const openclawDir = path.join(rootDir, ".openclaw");
145
148
  fs.mkdirSync(openclawDir, { recursive: true });
149
+ const { hourlyGitSyncPath } = migrateManagedInternalFiles({
150
+ fs,
151
+ openclawDir,
152
+ });
146
153
  console.log(`[alphaclaw] Root directory: ${rootDir}`);
147
154
 
148
155
  // Check for pending update marker (written by the update endpoint before restart).
@@ -542,7 +549,6 @@ if (!fs.existsSync(gogConfigFile)) {
542
549
  // 8. Install/reconcile system cron entry
543
550
  // ---------------------------------------------------------------------------
544
551
 
545
- const hourlyGitSyncPath = path.join(openclawDir, "hourly-git-sync.sh");
546
552
  const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");
547
553
 
548
554
  try {
@@ -822,7 +828,16 @@ if (fs.existsSync(configPath)) {
822
828
  const replacements = buildSecretReplacements(process.env);
823
829
  for (const [secret, envRef] of replacements) {
824
830
  if (secret) {
825
- content = content.split(secret).join(envRef);
831
+ // Only replace the secret if it is an exact match for a JSON string value
832
+ // This ensures we do not replace substrings inside other strings
833
+ const secretJson = JSON.stringify(secret);
834
+ content = content.replace(
835
+ new RegExp(
836
+ secretJson.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"),
837
+ "g",
838
+ ),
839
+ JSON.stringify(envRef),
840
+ );
826
841
  }
827
842
  }
828
843
  fs.writeFileSync(configPath, content);
@@ -105,6 +105,11 @@
105
105
  padding: 6px 0 8px;
106
106
  }
107
107
 
108
+ .file-tree-wrap-loading {
109
+ min-height: 100%;
110
+ display: flex;
111
+ }
112
+
108
113
  .file-tree-search {
109
114
  padding: 0 8px 6px;
110
115
  }
@@ -281,6 +286,14 @@
281
286
  box-shadow: 0 0 6px rgba(45, 226, 255, 0.75);
282
287
  }
283
288
 
289
+ .tree-lock-icon {
290
+ flex: 0 0 auto;
291
+ margin-left: auto;
292
+ width: 11px;
293
+ height: 11px;
294
+ color: var(--text-dim);
295
+ }
296
+
284
297
  .tree-children {
285
298
  list-style: none;
286
299
  }
@@ -295,6 +308,15 @@
295
308
  color: var(--text-muted);
296
309
  }
297
310
 
311
+ .file-tree-state-loading {
312
+ width: 100%;
313
+ flex: 1 1 auto;
314
+ min-height: 100%;
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ }
319
+
298
320
  .file-tree-state-error {
299
321
  color: #f87171;
300
322
  }
@@ -331,6 +353,21 @@
331
353
  background: rgba(234, 179, 8, 0.08);
332
354
  }
333
355
 
356
+ .file-viewer-protected-banner.is-locked {
357
+ background: rgba(220, 38, 38, 0.16);
358
+ }
359
+
360
+ .file-viewer-protected-banner-icon {
361
+ width: 14px;
362
+ height: 14px;
363
+ color: #fca5a5;
364
+ flex-shrink: 0;
365
+ }
366
+
367
+ .file-viewer-protected-banner.is-locked .file-viewer-protected-banner-text {
368
+ color: #fecaca;
369
+ }
370
+
334
371
  .file-viewer-protected-banner-text {
335
372
  font-size: 12px;
336
373
  color: #f7cc5e;
@@ -344,6 +381,15 @@
344
381
  letter-spacing: 0.01em;
345
382
  }
346
383
 
384
+ .file-viewer-diff-banner {
385
+ background: rgba(59, 130, 246, 0.12);
386
+ }
387
+
388
+ .file-viewer-diff-banner .file-viewer-protected-banner-text,
389
+ .file-viewer-diff-banner {
390
+ color: #bfdbfe;
391
+ }
392
+
347
393
  .file-viewer-tabbar-spacer {
348
394
  flex: 1;
349
395
  }
@@ -559,6 +605,48 @@
559
605
  align-items: stretch;
560
606
  }
561
607
 
608
+ .file-viewer-diff-shell {
609
+ width: 100%;
610
+ min-height: 0;
611
+ height: 100%;
612
+ overflow: auto;
613
+ padding: 8px 0;
614
+ }
615
+
616
+ .file-viewer-diff-pre {
617
+ margin: 0;
618
+ padding: 0 12px 12px;
619
+ font-family: inherit;
620
+ font-size: 12px;
621
+ line-height: 1.45;
622
+ color: var(--text-muted);
623
+ }
624
+
625
+ .file-viewer-diff-line {
626
+ white-space: pre-wrap;
627
+ word-break: break-word;
628
+ padding: 1px 8px;
629
+ border-radius: 4px;
630
+ }
631
+
632
+ .file-viewer-diff-line.is-added {
633
+ color: #86efac;
634
+ background: rgba(34, 197, 94, 0.1);
635
+ }
636
+
637
+ .file-viewer-diff-line.is-removed {
638
+ color: #fca5a5;
639
+ background: rgba(239, 68, 68, 0.1);
640
+ }
641
+
642
+ .file-viewer-diff-line.is-hunk {
643
+ color: #93c5fd;
644
+ }
645
+
646
+ .file-viewer-diff-line.is-header {
647
+ color: var(--text-dim);
648
+ }
649
+
562
650
  .file-viewer-editor-line-num-col {
563
651
  width: 56px;
564
652
  flex-shrink: 0;
@@ -975,17 +1063,110 @@
975
1063
  color: var(--text-muted);
976
1064
  }
977
1065
 
978
- .sidebar-git-list {
1066
+ .sidebar-git-changes-label {
1067
+ padding: 7px 10px 4px;
1068
+ font-size: 10px;
1069
+ letter-spacing: 0.06em;
1070
+ text-transform: uppercase;
1071
+ color: var(--text-dim);
1072
+ }
1073
+
1074
+ .sidebar-git-changes-list {
979
1075
  list-style: none;
980
1076
  display: flex;
981
1077
  flex-direction: column;
982
- gap: 3px;
983
- padding: 6px 10px 0;
1078
+ gap: 2px;
1079
+ padding: 0 6px;
1080
+ }
1081
+
1082
+ .sidebar-git-change-row {
1083
+ display: flex;
1084
+ align-items: center;
1085
+ justify-content: space-between;
1086
+ gap: 8px;
1087
+ min-height: 22px;
1088
+ padding: 2px 6px;
1089
+ border-radius: 6px;
1090
+ line-height: 1.25;
1091
+ }
1092
+
1093
+ .sidebar-git-change-row.is-clickable {
1094
+ cursor: pointer;
1095
+ }
1096
+
1097
+ .sidebar-git-change-row.is-clickable:hover {
1098
+ background: rgba(255, 255, 255, 0.04);
1099
+ }
1100
+
1101
+ .sidebar-git-change-path {
1102
+ min-width: 0;
1103
+ white-space: nowrap;
1104
+ overflow: hidden;
1105
+ text-overflow: ellipsis;
1106
+ color: var(--text-muted);
1107
+ font-weight: 400;
1108
+ }
1109
+
1110
+ .sidebar-git-change-meta {
1111
+ display: inline-flex;
1112
+ align-items: center;
1113
+ gap: 6px;
1114
+ flex-shrink: 0;
1115
+ font-size: 10px;
1116
+ }
1117
+
1118
+ .sidebar-git-change-plus {
1119
+ color: #71f8a7;
1120
+ }
1121
+
1122
+ .sidebar-git-change-minus {
1123
+ color: #f3a86a;
1124
+ }
1125
+
1126
+ .sidebar-git-change-status {
1127
+ font-size: 10px;
1128
+ letter-spacing: 0.06em;
1129
+ }
1130
+
1131
+ .sidebar-git-change-row.is-untracked .sidebar-git-change-status {
1132
+ color: #71f8a7;
1133
+ }
1134
+
1135
+ .sidebar-git-change-row.is-modified .sidebar-git-change-status {
1136
+ color: #63ebff;
1137
+ }
1138
+
1139
+ .sidebar-git-change-row.is-deleted .sidebar-git-change-status {
1140
+ color: #f87171;
1141
+ }
1142
+
1143
+ .sidebar-git-change-row.is-deleted .sidebar-git-change-path {
1144
+ text-decoration: line-through;
1145
+ text-decoration-thickness: 1px;
1146
+ }
1147
+
1148
+ .sidebar-git-scroll {
984
1149
  overflow-y: auto;
985
1150
  min-height: 0;
986
1151
  flex: 1 1 auto;
987
1152
  }
988
1153
 
1154
+ .sidebar-git-actions {
1155
+ padding: 8px 10px 6px;
1156
+ }
1157
+
1158
+ .sidebar-git-sync-button {
1159
+ width: 100%;
1160
+ }
1161
+
1162
+ .sidebar-git-list {
1163
+ list-style: none;
1164
+ display: flex;
1165
+ flex-direction: column;
1166
+ gap: 3px;
1167
+ padding: 6px 10px 0;
1168
+ }
1169
+
989
1170
  .sidebar-git-list li {
990
1171
  display: flex;
991
1172
  align-items: baseline;
@@ -8,7 +8,7 @@
8
8
  --border-strong: rgba(255, 255, 255, 0.11);
9
9
  --text: #c9d1d9;
10
10
  --text-muted: #6e7681;
11
- --text-dim: #383d47;
11
+ --text-dim: #424854;
12
12
  --accent: #63ebff;
13
13
  --accent-dim: rgba(99, 235, 255, 0.4);
14
14
  --accent-link: rgba(99, 235, 255, 0.6);
@@ -383,7 +383,9 @@ const App = () => {
383
383
  const [acDismissed, setAcDismissed] = useState(false);
384
384
  const [authEnabled, setAuthEnabled] = useState(false);
385
385
  const [menuOpen, setMenuOpen] = useState(false);
386
- const [sidebarTab, setSidebarTab] = useState("menu");
386
+ const [sidebarTab, setSidebarTab] = useState(() =>
387
+ location.startsWith("/browse") ? "browse" : "menu",
388
+ );
387
389
  const [sidebarWidthPx, setSidebarWidthPx] = useState(() => {
388
390
  const settings = readUiSettings();
389
391
  if (!Number.isFinite(settings.sidebarWidthPx)) return kDefaultSidebarWidthPx;
@@ -591,21 +593,24 @@ const App = () => {
591
593
  `;
592
594
  }
593
595
 
594
- const buildBrowseRoute = (relativePath) => {
596
+ const buildBrowseRoute = (relativePath, options = {}) => {
597
+ const view = String(options?.view || "edit");
595
598
  const encodedPath = String(relativePath || "")
596
599
  .split("/")
597
600
  .filter(Boolean)
598
601
  .map((segment) => encodeURIComponent(segment))
599
602
  .join("/");
600
- return encodedPath ? `/browse/${encodedPath}` : "/browse";
603
+ const baseRoute = encodedPath ? `/browse/${encodedPath}` : "/browse";
604
+ if (view === "diff" && encodedPath) return `${baseRoute}?view=diff`;
605
+ return baseRoute;
601
606
  };
602
607
  const navigateToSubScreen = (screen) => {
603
608
  setLocation(`/${screen}`);
604
609
  setMobileSidebarOpen(false);
605
610
  };
606
- const navigateToBrowseFile = (relativePath) => {
611
+ const navigateToBrowseFile = (relativePath, options = {}) => {
607
612
  setBrowsePreviewPath("");
608
- setLocation(buildBrowseRoute(relativePath));
613
+ setLocation(buildBrowseRoute(relativePath, options));
609
614
  setMobileSidebarOpen(false);
610
615
  };
611
616
  const handleSidebarLogout = async () => {
@@ -668,8 +673,13 @@ const App = () => {
668
673
  ];
669
674
 
670
675
  const isBrowseRoute = location.startsWith("/browse");
676
+ const browseRoutePath = isBrowseRoute ? String(location || "").split("?")[0] : "";
677
+ const browseRouteQuery =
678
+ isBrowseRoute && String(location || "").includes("?")
679
+ ? String(location || "").split("?").slice(1).join("?")
680
+ : "";
671
681
  const selectedBrowsePath = isBrowseRoute
672
- ? location
682
+ ? browseRoutePath
673
683
  .replace(/^\/browse\/?/, "")
674
684
  .split("/")
675
685
  .filter(Boolean)
@@ -682,6 +692,12 @@ const App = () => {
682
692
  })
683
693
  .join("/")
684
694
  : "";
695
+ const activeBrowsePath = browsePreviewPath || selectedBrowsePath;
696
+ const browseViewerMode =
697
+ !browsePreviewPath &&
698
+ new URLSearchParams(browseRouteQuery).get("view") === "diff"
699
+ ? "diff"
700
+ : "edit";
685
701
  const selectedNavId = isBrowseRoute
686
702
  ? "browse"
687
703
  : location === "/telegram"
@@ -727,6 +743,28 @@ const App = () => {
727
743
  );
728
744
  }, [isBrowseRoute, selectedBrowsePath]);
729
745
 
746
+ useEffect(() => {
747
+ const handleBrowseGitSynced = () => {
748
+ if (!isBrowseRoute || browseViewerMode !== "diff") return;
749
+ const activePath = String(selectedBrowsePath || "").trim();
750
+ if (!activePath) return;
751
+ setLocation(buildBrowseRoute(activePath, { view: "edit" }));
752
+ };
753
+ window.addEventListener("alphaclaw:browse-git-synced", handleBrowseGitSynced);
754
+ return () => {
755
+ window.removeEventListener(
756
+ "alphaclaw:browse-git-synced",
757
+ handleBrowseGitSynced,
758
+ );
759
+ };
760
+ }, [
761
+ isBrowseRoute,
762
+ browseViewerMode,
763
+ selectedBrowsePath,
764
+ setLocation,
765
+ buildBrowseRoute,
766
+ ]);
767
+
730
768
  useEffect(() => {
731
769
  const settings = readUiSettings();
732
770
  settings.sidebarWidthPx = sidebarWidthPx;
@@ -848,11 +886,23 @@ const App = () => {
848
886
  ${isBrowseRoute
849
887
  ? html`
850
888
  <${FileViewer}
851
- filePath=${browsePreviewPath || selectedBrowsePath}
889
+ filePath=${activeBrowsePath}
852
890
  isPreviewOnly=${Boolean(
853
891
  browsePreviewPath &&
854
892
  browsePreviewPath !== selectedBrowsePath,
855
893
  )}
894
+ browseView=${browseViewerMode}
895
+ onRequestEdit=${(targetPath) => {
896
+ const normalizedTargetPath = String(targetPath || "");
897
+ if (
898
+ normalizedTargetPath &&
899
+ normalizedTargetPath !== selectedBrowsePath
900
+ ) {
901
+ navigateToBrowseFile(normalizedTargetPath, { view: "edit" });
902
+ return;
903
+ }
904
+ setLocation(buildBrowseRoute(selectedBrowsePath, { view: "edit" }));
905
+ }}
856
906
  />
857
907
  `
858
908
  : html`
@@ -1,5 +1,11 @@
1
1
  import { h } from "https://esm.sh/preact";
2
- import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "https://esm.sh/preact/hooks";
3
9
  import htm from "https://esm.sh/htm";
4
10
  import { fetchBrowseTree } from "../lib/api.js";
5
11
  import {
@@ -17,14 +23,45 @@ import {
17
23
  FileCodeLineIcon,
18
24
  Database2LineIcon,
19
25
  HashtagIcon,
26
+ LockLineIcon,
20
27
  } from "./icons.js";
28
+ import { LoadingSpinner } from "./loading-spinner.js";
21
29
 
22
30
  const html = htm.bind(h);
23
31
  const kTreeIndentPx = 9;
24
32
  const kFolderBasePaddingPx = 10;
25
33
  const kFileBasePaddingPx = 14;
34
+ const kTreeRefreshIntervalMs = 5000;
26
35
  const kCollapsedFoldersStorageKey = "alphaclaw.browse.collapsedFolders";
27
36
  const kLegacyCollapsedFoldersStorageKey = "alphaclawBrowseCollapsedFolders";
37
+ const kLockedBrowsePaths = new Set([
38
+ "hooks/bootstrap/agents.md",
39
+ "hooks/bootstrap/tools.md",
40
+ ".alphaclaw/hourly-git-sync.sh",
41
+ ".alphaclaw/.cli-device-auto-approved",
42
+ ]);
43
+
44
+ const normalizePolicyPath = (inputPath) =>
45
+ String(inputPath || "")
46
+ .replaceAll("\\", "/")
47
+ .replace(/^\.\/+/, "")
48
+ .replace(/^\/+/, "")
49
+ .trim()
50
+ .toLowerCase();
51
+
52
+ const matchesPolicyPath = (policyPathSet, normalizedPath) => {
53
+ const safeNormalizedPath = String(normalizedPath || "").trim();
54
+ if (!safeNormalizedPath) return false;
55
+ for (const policyPath of policyPathSet) {
56
+ if (
57
+ safeNormalizedPath === policyPath ||
58
+ safeNormalizedPath.endsWith(`/${policyPath}`)
59
+ ) {
60
+ return true;
61
+ }
62
+ }
63
+ return false;
64
+ };
28
65
 
29
66
  const readStoredCollapsedPaths = () => {
30
67
  try {
@@ -165,6 +202,10 @@ const TreeNode = ({
165
202
  const isActive = selectedPath === node.path;
166
203
  const isSearchActiveNode = searchActivePath === node.path;
167
204
  const hasDraft = draftPaths.has(node.path || "");
205
+ const isLocked = matchesPolicyPath(
206
+ kLockedBrowsePaths,
207
+ normalizePolicyPath(node.path || ""),
208
+ );
168
209
  const fileIconMeta = getFileIconMeta(node.name);
169
210
  const FileTypeIcon = fileIconMeta.icon;
170
211
  return html`
@@ -179,7 +220,14 @@ const TreeNode = ({
179
220
  >
180
221
  <${FileTypeIcon} className=${fileIconMeta.className} />
181
222
  <span class="tree-label">${node.name}</span>
182
- ${hasDraft ? html`<span class="tree-draft-dot" aria-hidden="true"></span>` : null}
223
+ ${isLocked
224
+ ? html`<${LockLineIcon}
225
+ className="tree-lock-icon"
226
+ title="Managed by Alpha Claw"
227
+ />`
228
+ : hasDraft
229
+ ? html`<span class="tree-draft-dot" aria-hidden="true"></span>`
230
+ : null}
183
231
  </a>
184
232
  </li>
185
233
  `;
@@ -237,34 +285,55 @@ export const FileTree = ({
237
285
  const [searchQuery, setSearchQuery] = useState("");
238
286
  const [searchActivePath, setSearchActivePath] = useState("");
239
287
  const searchInputRef = useRef(null);
288
+ const treeSignatureRef = useRef("");
240
289
 
241
- useEffect(() => {
242
- let active = true;
243
- const loadTree = async () => {
244
- setLoading(true);
245
- setError("");
246
- try {
247
- const data = await fetchBrowseTree();
248
- if (!active) return;
249
- setTreeRoot(data.root || null);
250
- setCollapsedPaths((previousPaths) => {
251
- if (previousPaths instanceof Set) return previousPaths;
252
- const nextPaths = new Set();
253
- collectFolderPaths(data.root, nextPaths);
254
- return nextPaths;
255
- });
256
- } catch (loadError) {
257
- if (!active) return;
290
+ const loadTree = useCallback(async ({ showLoading = false } = {}) => {
291
+ if (showLoading) setLoading(true);
292
+ if (showLoading) setError("");
293
+ try {
294
+ const data = await fetchBrowseTree();
295
+ const nextRoot = data.root || null;
296
+ const nextSignature = JSON.stringify(nextRoot || {});
297
+ if (treeSignatureRef.current !== nextSignature) {
298
+ treeSignatureRef.current = nextSignature;
299
+ setTreeRoot(nextRoot);
300
+ }
301
+ setCollapsedPaths((previousPaths) => {
302
+ if (previousPaths instanceof Set) return previousPaths;
303
+ const nextPaths = new Set();
304
+ collectFolderPaths(nextRoot, nextPaths);
305
+ return nextPaths;
306
+ });
307
+ if (showLoading) setError("");
308
+ } catch (loadError) {
309
+ if (showLoading) {
258
310
  setError(loadError.message || "Could not load file tree");
259
- } finally {
260
- if (active) setLoading(false);
261
311
  }
312
+ } finally {
313
+ if (showLoading) setLoading(false);
314
+ }
315
+ }, []);
316
+
317
+ useEffect(() => {
318
+ loadTree({ showLoading: true });
319
+ }, [loadTree]);
320
+
321
+ useEffect(() => {
322
+ const refreshTree = () => {
323
+ loadTree({ showLoading: false });
262
324
  };
263
- loadTree();
325
+ const refreshInterval = window.setInterval(
326
+ refreshTree,
327
+ kTreeRefreshIntervalMs,
328
+ );
329
+ window.addEventListener("alphaclaw:browse-file-saved", refreshTree);
330
+ window.addEventListener("alphaclaw:browse-tree-refresh", refreshTree);
264
331
  return () => {
265
- active = false;
332
+ window.clearInterval(refreshInterval);
333
+ window.removeEventListener("alphaclaw:browse-file-saved", refreshTree);
334
+ window.removeEventListener("alphaclaw:browse-tree-refresh", refreshTree);
266
335
  };
267
- }, []);
336
+ }, [loadTree]);
268
337
 
269
338
  const normalizedSearchQuery = String(searchQuery || "").trim().toLowerCase();
270
339
  const rootChildren = useMemo(() => {
@@ -430,7 +499,13 @@ export const FileTree = ({
430
499
  };
431
500
 
432
501
  if (loading) {
433
- return html`<div class="file-tree-state">Loading files...</div>`;
502
+ return html`
503
+ <div class="file-tree-wrap file-tree-wrap-loading">
504
+ <div class="file-tree-state file-tree-state-loading">
505
+ <${LoadingSpinner} className="h-5 w-5 text-gray-400" />
506
+ </div>
507
+ </div>
508
+ `;
434
509
  }
435
510
  if (error) {
436
511
  return html`<div class="file-tree-state file-tree-state-error">