@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
@@ -119,6 +119,19 @@ export const Image2FillIcon = ({ className = "" }) => html`
119
119
  </svg>
120
120
  `;
121
121
 
122
+ export const FileMusicLineIcon = ({ className = "" }) => html`
123
+ <svg
124
+ class=${className}
125
+ viewBox="0 0 24 24"
126
+ fill="currentColor"
127
+ aria-hidden="true"
128
+ >
129
+ <path
130
+ d="M16 8V10H13V14.5C13 15.8807 11.8807 17 10.5 17C9.11929 17 8 15.8807 8 14.5C8 13.1193 9.11929 12 10.5 12C10.6712 12 10.8384 12.0172 11 12.05V8H15V4H5V20H19V8H16ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918Z"
131
+ />
132
+ </svg>
133
+ `;
134
+
122
135
  export const TerminalFillIcon = ({ className = "" }) => html`
123
136
  <svg
124
137
  class=${className}
@@ -222,3 +235,16 @@ export const SaveFillIcon = ({ className = "" }) => html`
222
235
  />
223
236
  </svg>
224
237
  `;
238
+
239
+ export const LockLineIcon = ({ className = "" }) => html`
240
+ <svg
241
+ class=${className}
242
+ viewBox="0 0 24 24"
243
+ fill="currentColor"
244
+ aria-hidden="true"
245
+ >
246
+ <path
247
+ d="M19 10H20C20.5523 10 21 10.4477 21 11V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V11C3 10.4477 3.44772 10 4 10H5V9C5 5.13401 8.13401 2 12 2C15.866 2 19 5.13401 19 9V10ZM5 12V20H19V12H5ZM11 14H13V18H11V14ZM17 10V9C17 6.23858 14.7614 4 12 4C9.23858 4 7 6.23858 7 9V10H17Z"
248
+ />
249
+ </svg>
250
+ `;
@@ -1,12 +1,16 @@
1
1
  import { h } from "https://esm.sh/preact";
2
2
  import { useEffect, useState } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
- import { fetchBrowseGitSummary } from "../lib/api.js";
4
+ import { fetchBrowseGitSummary, syncBrowseChanges } from "../lib/api.js";
5
+ import { ActionButton } from "./action-button.js";
5
6
  import { GitBranchLineIcon, GithubFillIcon } from "./icons.js";
6
7
  import { LoadingSpinner } from "./loading-spinner.js";
8
+ import { showToast } from "./toast.js";
7
9
 
8
10
  const html = htm.bind(h);
9
11
  const kRefreshMs = 10000;
12
+ const kSyncCommitFileNameLimit = 4;
13
+ const kCommitHistoryLimit = 12;
10
14
 
11
15
  const formatCommitTime = (unixSeconds) => {
12
16
  if (!unixSeconds) return "";
@@ -25,8 +29,106 @@ const getRepoName = (summary) => {
25
29
  return segment || "repo";
26
30
  };
27
31
 
28
- export const SidebarGitPanel = () => {
32
+ const getChangedFilePresentation = (changedFile) => {
33
+ const statusKind = String(changedFile?.statusKind || "M").toUpperCase();
34
+ if (statusKind === "U") {
35
+ return {
36
+ statusLabel: "U",
37
+ statusClass: "is-untracked",
38
+ rowClass: "is-clickable",
39
+ canOpen: true,
40
+ };
41
+ }
42
+ if (statusKind === "D") {
43
+ return {
44
+ statusLabel: "D",
45
+ statusClass: "is-deleted",
46
+ rowClass: "",
47
+ canOpen: false,
48
+ };
49
+ }
50
+ return {
51
+ statusLabel: "M",
52
+ statusClass: "is-modified",
53
+ rowClass: "is-clickable",
54
+ canOpen: true,
55
+ };
56
+ };
57
+
58
+ const formatDelta = (value, prefix) => {
59
+ if (value === null || value === undefined || value === "") return "";
60
+ const numericValue = Number(value);
61
+ if (!Number.isFinite(numericValue) || numericValue <= 0) return "";
62
+ return `${prefix}${numericValue}`;
63
+ };
64
+
65
+ const getRemoteSyncPresentation = (summary) => {
66
+ const safeState = String(summary?.syncState || "").trim();
67
+ const aheadCount = Number(summary?.aheadCount) || 0;
68
+ const behindCount = Number(summary?.behindCount) || 0;
69
+ if (safeState === "ahead") {
70
+ return {
71
+ label: "↑",
72
+ title: `Ahead by ${aheadCount}`,
73
+ className: "is-ahead",
74
+ };
75
+ }
76
+ if (safeState === "behind") {
77
+ return {
78
+ label: "↓",
79
+ title: `Behind by ${behindCount}`,
80
+ className: "is-behind",
81
+ };
82
+ }
83
+ if (safeState === "diverged") {
84
+ return {
85
+ label: "↕",
86
+ title: `Diverged (${aheadCount} ahead, ${behindCount} behind)`,
87
+ className: "is-diverged",
88
+ };
89
+ }
90
+ if (safeState === "upstream-gone") {
91
+ return {
92
+ label: "!",
93
+ title: "Upstream missing",
94
+ className: "is-upstream-gone",
95
+ };
96
+ }
97
+ if (safeState === "no-upstream" || !summary?.hasUpstream) {
98
+ return {
99
+ label: "!",
100
+ title: "Not linked",
101
+ className: "is-no-upstream",
102
+ };
103
+ }
104
+ return {
105
+ label: "",
106
+ title: "Up to date",
107
+ className: "is-up-to-date",
108
+ };
109
+ };
110
+
111
+ const buildSyncCommitMessage = (changedFiles) => {
112
+ const filePaths = Array.isArray(changedFiles)
113
+ ? changedFiles
114
+ .map((file) => String(file?.path || "").trim())
115
+ .filter(Boolean)
116
+ : [];
117
+ const totalCount = filePaths.length;
118
+ if (totalCount <= 0) return "sync changes";
119
+
120
+ const fileNames = filePaths.map((filePath) => filePath.split("/").filter(Boolean).pop() || filePath);
121
+ const uniqueFileNames = Array.from(new Set(fileNames));
122
+ const shownFileNames = uniqueFileNames.slice(0, kSyncCommitFileNameLimit);
123
+ const remainingCount = Math.max(0, uniqueFileNames.length - shownFileNames.length);
124
+ const noun = totalCount === 1 ? "file" : "files";
125
+ const suffix = remainingCount > 0 ? ` +${remainingCount} more` : "";
126
+ return `Edited ${totalCount} ${noun} - ${shownFileNames.join(", ")}${suffix}`;
127
+ };
128
+
129
+ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
29
130
  const [loading, setLoading] = useState(true);
131
+ const [syncing, setSyncing] = useState(false);
30
132
  const [error, setError] = useState("");
31
133
  const [summary, setSummary] = useState(null);
32
134
 
@@ -84,6 +186,32 @@ export const SidebarGitPanel = () => {
84
186
  `;
85
187
  }
86
188
 
189
+ const hasUncommittedChanges = (summary.changedFiles || []).length > 0;
190
+ const aheadCount = Number(summary?.aheadCount) || 0;
191
+ const canSyncChanges = hasUncommittedChanges || aheadCount > 0;
192
+ const remoteSync = getRemoteSyncPresentation(summary);
193
+ const handleSyncChanges = async () => {
194
+ if (!canSyncChanges || syncing) return;
195
+ try {
196
+ setSyncing(true);
197
+ const commitMessage = buildSyncCommitMessage(summary?.changedFiles || []);
198
+ const syncResult = await syncBrowseChanges(commitMessage);
199
+ if (syncResult?.committed || syncResult?.pushed) {
200
+ window.dispatchEvent(new CustomEvent("alphaclaw:browse-git-synced"));
201
+ showToast(syncResult.message || "Changes synced", "success");
202
+ } else {
203
+ showToast(syncResult?.message || "No changes to sync", "info");
204
+ }
205
+ const nextSummary = await fetchBrowseGitSummary();
206
+ setSummary(nextSummary);
207
+ setError("");
208
+ } catch (syncError) {
209
+ showToast(syncError.message || "Could not sync changes", "error");
210
+ } finally {
211
+ setSyncing(false);
212
+ }
213
+ };
214
+
87
215
  return html`
88
216
  <div class="sidebar-git-panel">
89
217
  <div class="sidebar-git-bar">
@@ -112,38 +240,98 @@ export const SidebarGitPanel = () => {
112
240
  <${GitBranchLineIcon} className="sidebar-git-bar-icon" />
113
241
  <span class="sidebar-git-branch">${summary.branch || "unknown"}</span>
114
242
  </span>
115
- <span class=${`sidebar-git-dirty ${summary.isDirty ? "is-dirty" : "is-clean"}`}>
116
- ${summary.isDirty ? "dirty" : "clean"}
117
- </span>
243
+ ${remoteSync.label
244
+ ? html`
245
+ <span
246
+ class=${`sidebar-git-sync-status ${remoteSync.className}`.trim()}
247
+ title=${remoteSync.title || ""}
248
+ aria-label=${remoteSync.title || ""}
249
+ >
250
+ ${remoteSync.label}
251
+ </span>
252
+ `
253
+ : null}
118
254
  </div>
119
- ${(summary.commits || []).length > 0
120
- ? html`
121
- <ul class="sidebar-git-list">
122
- ${(summary.commits || []).slice(0, 4).map(
123
- (commit) => html`
124
- <li title=${formatCommitTime(commit.timestamp)}>
125
- ${commit.url
126
- ? html`
127
- <a
128
- class="sidebar-git-commit-link"
129
- href=${commit.url}
130
- target="_blank"
131
- rel="noopener noreferrer"
132
- >
255
+ <div class="sidebar-git-scroll">
256
+ ${(summary.changedFiles || []).length > 0
257
+ ? html`
258
+ <div class="sidebar-git-changes-label">
259
+ ${`Unsynced Changes (${summary.changedFilesCount || (summary.changedFiles || []).length})`}
260
+ </div>
261
+ <ul class="sidebar-git-changes-list">
262
+ ${(summary.changedFiles || []).map((changedFile) => {
263
+ const presentation = getChangedFilePresentation(changedFile);
264
+ const changedPath = String(changedFile?.path || "");
265
+ const plusDelta = formatDelta(changedFile?.addedLines, "+");
266
+ const minusDelta = formatDelta(changedFile?.deletedLines, "-");
267
+ return html`
268
+ <li
269
+ class=${`sidebar-git-change-row ${presentation.statusClass} ${presentation.rowClass}`.trim()}
270
+ title=${changedPath}
271
+ onclick=${() => {
272
+ if (!presentation.canOpen || !changedPath) return;
273
+ onSelectFile(changedPath, { view: "diff" });
274
+ }}
275
+ >
276
+ <span class="sidebar-git-change-path">${changedPath}</span>
277
+ <span class="sidebar-git-change-meta">
278
+ ${plusDelta
279
+ ? html`<span class="sidebar-git-change-plus">${plusDelta}</span>`
280
+ : null}
281
+ ${minusDelta
282
+ ? html`<span class="sidebar-git-change-minus">${minusDelta}</span>`
283
+ : null}
284
+ <span class="sidebar-git-change-status">${presentation.statusLabel}</span>
285
+ </span>
286
+ </li>
287
+ `;
288
+ })}
289
+ </ul>
290
+ <div class="sidebar-git-actions">
291
+ <${ActionButton}
292
+ onClick=${handleSyncChanges}
293
+ disabled=${!canSyncChanges}
294
+ loading=${syncing}
295
+ loadingMode="inline"
296
+ idleLabel="Sync Changes"
297
+ loadingLabel="Syncing..."
298
+ tone="primary"
299
+ size="sm"
300
+ className="sidebar-git-sync-button"
301
+ />
302
+ </div>
303
+ `
304
+ : null}
305
+ ${(summary.commits || []).length > 0
306
+ ? html`
307
+ <div class="sidebar-git-changes-label">commit history</div>
308
+ <ul class="sidebar-git-list">
309
+ ${(summary.commits || []).slice(0, kCommitHistoryLimit).map(
310
+ (commit) => html`
311
+ <li title=${formatCommitTime(commit.timestamp)}>
312
+ ${commit.url
313
+ ? html`
314
+ <a
315
+ class="sidebar-git-commit-link"
316
+ href=${commit.url}
317
+ target="_blank"
318
+ rel="noopener noreferrer"
319
+ >
320
+ <span class="sidebar-git-hash">${commit.shortHash}</span>
321
+ <span>${commit.message}</span>
322
+ </a>
323
+ `
324
+ : html`
133
325
  <span class="sidebar-git-hash">${commit.shortHash}</span>
134
326
  <span>${commit.message}</span>
135
- </a>
136
- `
137
- : html`
138
- <span class="sidebar-git-hash">${commit.shortHash}</span>
139
- <span>${commit.message}</span>
140
- `}
141
- </li>
142
- `,
143
- )}
144
- </ul>
145
- `
146
- : null}
327
+ `}
328
+ </li>
329
+ `,
330
+ )}
331
+ </ul>
332
+ `
333
+ : null}
334
+ </div>
147
335
  </div>
148
336
  `;
149
337
  };
@@ -214,7 +214,7 @@ export const AppSidebar = ({
214
214
  ref=${browseBottomPanelRef}
215
215
  style=${{ height: `${browseBottomPanelHeightPx}px` }}
216
216
  >
217
- <${SidebarGitPanel} />
217
+ <${SidebarGitPanel} onSelectFile=${onSelectBrowseFile} />
218
218
  ${acHasUpdate && acLatest && !acDismissed
219
219
  ? html`
220
220
  <${UpdateActionButton}
@@ -122,7 +122,10 @@ const kUsageMetricUiSettingKey = "usageMetric";
122
122
  const SummaryCard = ({ title, tokens, cost }) => html`
123
123
  <div class="bg-surface border border-border rounded-xl p-4">
124
124
  <h3 class="card-label text-xs">${title}</h3>
125
- <div class="text-lg font-semibold mt-1">${formatTokens(tokens)} tokens</div>
125
+ <div class="text-lg font-semibold mt-1">
126
+ ${formatTokens(tokens)}
127
+ <span class="text-xs text-[var(--text-muted)] ml-1">tokens</span>
128
+ </div>
126
129
  <div class="text-xs text-[var(--text-muted)] mt-1">${formatUsd(cost)}</div>
127
130
  </div>
128
131
  `;
@@ -28,8 +28,6 @@ const formatBytes = (bytes) => {
28
28
 
29
29
  const barColor = (percent) => {
30
30
  if (percent == null) return "bg-gray-600";
31
- if (percent >= 90) return "bg-red-500";
32
- if (percent >= 70) return "bg-yellow-400";
33
31
  return "bg-cyan-400";
34
32
  };
35
33
 
@@ -139,6 +137,9 @@ export const WatchdogTab = ({
139
137
  restartingGateway = false,
140
138
  onRestartGateway,
141
139
  restartSignal = 0,
140
+ openclawUpdateInProgress = false,
141
+ onOpenclawVersionActionComplete = () => {},
142
+ onOpenclawUpdate,
142
143
  }) => {
143
144
  const eventsPoll = usePolling(() => fetchWatchdogEvents(20), 15000);
144
145
  const resourcesPoll = usePolling(() => fetchWatchdogResources(), 5000);
@@ -302,6 +303,9 @@ export const WatchdogTab = ({
302
303
  watchdogStatus=${currentWatchdogStatus}
303
304
  onRepair=${onRepair}
304
305
  repairing=${isRepairInProgress}
306
+ openclawUpdateInProgress=${openclawUpdateInProgress}
307
+ onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
308
+ onOpenclawUpdate=${onOpenclawUpdate}
305
309
  />
306
310
 
307
311
  ${(() => {
@@ -393,3 +393,34 @@ export const fetchBrowseGitSummary = async () => {
393
393
  const res = await authFetch('/api/browse/git-summary');
394
394
  return parseJsonOrThrow(res, 'Could not load git summary');
395
395
  };
396
+
397
+ export const fetchBrowseFileDiff = async (filePath) => {
398
+ const params = new URLSearchParams({ path: String(filePath || "") });
399
+ const res = await authFetch(`/api/browse/git-diff?${params.toString()}`);
400
+ return parseJsonOrThrow(res, 'Could not load file diff');
401
+ };
402
+
403
+ export const fetchBrowseSqliteTable = async ({
404
+ filePath,
405
+ table,
406
+ limit = 50,
407
+ offset = 0,
408
+ }) => {
409
+ const params = new URLSearchParams({
410
+ path: String(filePath || ""),
411
+ table: String(table || ""),
412
+ limit: String(limit),
413
+ offset: String(offset),
414
+ });
415
+ const res = await authFetch(`/api/browse/sqlite-table?${params.toString()}`);
416
+ return parseJsonOrThrow(res, "Could not load sqlite table data");
417
+ };
418
+
419
+ export const syncBrowseChanges = async (message = "") => {
420
+ const res = await authFetch('/api/browse/git-sync', {
421
+ method: 'POST',
422
+ headers: { 'Content-Type': 'application/json' },
423
+ body: JSON.stringify({ message: String(message || "") }),
424
+ });
425
+ return parseJsonOrThrow(res, 'Could not sync changes');
426
+ };
@@ -0,0 +1,34 @@
1
+ export const kProtectedBrowsePaths = new Set([
2
+ "openclaw.json",
3
+ "devices/paired.json",
4
+ ]);
5
+
6
+ export const kLockedBrowsePaths = new Set([
7
+ "hooks/bootstrap/agents.md",
8
+ "hooks/bootstrap/tools.md",
9
+ "skills/control-ui/skill.md",
10
+ ".alphaclaw/hourly-git-sync.sh",
11
+ ".alphaclaw/.cli-device-auto-approved",
12
+ ]);
13
+
14
+ export const normalizeBrowsePolicyPath = (inputPath) =>
15
+ String(inputPath || "")
16
+ .replaceAll("\\", "/")
17
+ .replace(/^\.\/+/, "")
18
+ .replace(/^\/+/, "")
19
+ .trim()
20
+ .toLowerCase();
21
+
22
+ export const matchesBrowsePolicyPath = (policyPathSet, normalizedPath) => {
23
+ const safeNormalizedPath = String(normalizedPath || "").trim();
24
+ if (!safeNormalizedPath) return false;
25
+ for (const policyPath of policyPathSet) {
26
+ if (
27
+ safeNormalizedPath === policyPath ||
28
+ safeNormalizedPath.endsWith(`/${policyPath}`)
29
+ ) {
30
+ return true;
31
+ }
32
+ }
33
+ return false;
34
+ };
@@ -0,0 +1,40 @@
1
+ #!/bin/bash
2
+ # Git auth shim -- injects GITHUB_TOKEN credentials for network operations
3
+ # inside the configured OpenClaw repo so the agent can use plain git commands.
4
+
5
+ REAL_GIT="@@REAL_GIT@@"
6
+ OPENCLAW_REPO_ROOT="@@OPENCLAW_REPO_ROOT@@"
7
+ ASKPASS_PATH="/tmp/alphaclaw-git-askpass.sh"
8
+
9
+ SUBCMD=""
10
+ for arg in "$@"; do
11
+ case "$arg" in
12
+ -*) ;;
13
+ *) SUBCMD="$arg"; break ;;
14
+ esac
15
+ done
16
+
17
+ needs_auth() {
18
+ case "$SUBCMD" in
19
+ push|pull|fetch|clone|ls-remote) return 0 ;;
20
+ *) return 1 ;;
21
+ esac
22
+ }
23
+
24
+ in_openclaw_root() {
25
+ if [ -z "$OPENCLAW_REPO_ROOT" ]; then
26
+ return 1
27
+ fi
28
+ case "$(pwd)" in
29
+ "$OPENCLAW_REPO_ROOT"|"${OPENCLAW_REPO_ROOT}"/*) return 0 ;;
30
+ *) return 1 ;;
31
+ esac
32
+ }
33
+
34
+ if [ "${ALPHACLAW_GIT_NO_AUTH:-}" = "1" ] || [ -z "${GITHUB_TOKEN:-}" ] || ! needs_auth || ! in_openclaw_root; then
35
+ exec "$REAL_GIT" "$@"
36
+ fi
37
+
38
+ export GIT_TERMINAL_PROMPT=0
39
+ export GIT_ASKPASS="$ASKPASS_PATH"
40
+ exec "$REAL_GIT" "$@"
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env sh
2
+ case "$1" in
3
+ *Username*) echo "x-access-token" ;;
4
+ *Password*) echo "${GITHUB_TOKEN:-}" ;;
5
+ *) echo "" ;;
6
+ esac
@@ -274,6 +274,13 @@ const kChannelDefs = {
274
274
  telegram: { envKey: "TELEGRAM_BOT_TOKEN" },
275
275
  discord: { envKey: "DISCORD_BOT_TOKEN" },
276
276
  };
277
+ const kLockedBrowsePaths = new Set([
278
+ "hooks/bootstrap/agents.md",
279
+ "hooks/bootstrap/tools.md",
280
+ "skills/control-ui/skill.md",
281
+ ".alphaclaw/hourly-git-sync.sh",
282
+ ".alphaclaw/.cli-device-auto-approved",
283
+ ]);
277
284
 
278
285
  const SETUP_API_PREFIXES = [
279
286
  "/api/status",
@@ -343,6 +350,7 @@ module.exports = {
343
350
  kSystemVars,
344
351
  kKnownVars,
345
352
  kKnownKeys,
353
+ kLockedBrowsePaths,
346
354
  SCOPE_MAP,
347
355
  REVERSE_SCOPE_MAP,
348
356
  BASE_SCOPES,
@@ -8,7 +8,11 @@ const {
8
8
 
9
9
  const normalizeOpenclawVersion = (rawVersion) => {
10
10
  if (!rawVersion) return null;
11
- return String(rawVersion).trim().replace(/^openclaw\s*/i, "") || null;
11
+ return (
12
+ String(rawVersion)
13
+ .trim()
14
+ .replace(/^openclaw\s*/i, "") || null
15
+ );
12
16
  };
13
17
 
14
18
  const compareVersionParts = (a, b) => {
@@ -62,9 +66,14 @@ const getCodexAccountId = (accessToken) => {
62
66
  const normalizeIp = (ip) => String(ip || "").replace(/^::ffff:/, "");
63
67
 
64
68
  const isTruthyEnvFlag = (value) =>
65
- ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
69
+ ["1", "true", "yes", "on"].includes(
70
+ String(value || "")
71
+ .trim()
72
+ .toLowerCase(),
73
+ );
66
74
  const isDebugEnabled = () =>
67
- isTruthyEnvFlag(process.env.ALPHACLAW_DEBUG) || isTruthyEnvFlag(process.env.DEBUG);
75
+ isTruthyEnvFlag(process.env.ALPHACLAW_DEBUG) ||
76
+ isTruthyEnvFlag(process.env.DEBUG);
68
77
 
69
78
  const getClientKey = (req) =>
70
79
  normalizeIp(
@@ -165,9 +174,13 @@ const readGoogleCredentials = () => {
165
174
  try {
166
175
  const c = JSON.parse(fs.readFileSync(GOG_CREDENTIALS_PATH, "utf8"));
167
176
  return {
168
- clientId: c.web?.client_id || c.installed?.client_id || c.client_id || null,
177
+ clientId:
178
+ c.web?.client_id || c.installed?.client_id || c.client_id || null,
169
179
  clientSecret:
170
- c.web?.client_secret || c.installed?.client_secret || c.client_secret || null,
180
+ c.web?.client_secret ||
181
+ c.installed?.client_secret ||
182
+ c.client_secret ||
183
+ null,
171
184
  };
172
185
  } catch {
173
186
  return { clientId: null, clientSecret: null };
@@ -0,0 +1,93 @@
1
+ const path = require("path");
2
+
3
+ const kInternalDirName = ".alphaclaw";
4
+ const kHourlyGitSyncFileName = "hourly-git-sync.sh";
5
+ const kCliDeviceAutoApprovedFileName = ".cli-device-auto-approved";
6
+
7
+ const buildManagedPaths = ({ openclawDir, pathModule = path }) => {
8
+ const internalDir = pathModule.join(openclawDir, kInternalDirName);
9
+ return {
10
+ internalDir,
11
+ hourlyGitSyncPath: pathModule.join(internalDir, kHourlyGitSyncFileName),
12
+ cliDeviceAutoApprovedPath: pathModule.join(
13
+ internalDir,
14
+ kCliDeviceAutoApprovedFileName,
15
+ ),
16
+ legacyHourlyGitSyncPath: pathModule.join(openclawDir, kHourlyGitSyncFileName),
17
+ legacyCliDeviceAutoApprovedPath: pathModule.join(
18
+ openclawDir,
19
+ kCliDeviceAutoApprovedFileName,
20
+ ),
21
+ };
22
+ };
23
+
24
+ const moveFile = ({ fs, sourcePath, targetPath, mode }) => {
25
+ try {
26
+ fs.renameSync(sourcePath, targetPath);
27
+ return true;
28
+ } catch {
29
+ fs.copyFileSync(sourcePath, targetPath);
30
+ if (Number.isFinite(mode)) {
31
+ fs.chmodSync(targetPath, mode);
32
+ }
33
+ fs.rmSync(sourcePath, { force: true });
34
+ return true;
35
+ }
36
+ };
37
+
38
+ const migrateManagedInternalFiles = ({
39
+ fs,
40
+ openclawDir,
41
+ pathModule = path,
42
+ logger = console,
43
+ }) => {
44
+ const managedPaths = buildManagedPaths({ openclawDir, pathModule });
45
+ fs.mkdirSync(managedPaths.internalDir, { recursive: true });
46
+
47
+ const migrateOne = ({ sourcePaths, targetPath }) => {
48
+ const existingSourcePath = sourcePaths.find((sourcePath) => fs.existsSync(sourcePath));
49
+ if (fs.existsSync(targetPath)) {
50
+ sourcePaths.forEach((sourcePath) => {
51
+ if (sourcePath !== targetPath && fs.existsSync(sourcePath)) {
52
+ fs.rmSync(sourcePath, { force: true });
53
+ }
54
+ });
55
+ return;
56
+ }
57
+ if (!existingSourcePath) return;
58
+ const sourceStats = fs.statSync(existingSourcePath);
59
+ moveFile({
60
+ fs,
61
+ sourcePath: existingSourcePath,
62
+ targetPath,
63
+ mode: sourceStats.mode,
64
+ });
65
+ sourcePaths.forEach((sourcePath) => {
66
+ if (sourcePath !== existingSourcePath && sourcePath !== targetPath && fs.existsSync(sourcePath)) {
67
+ fs.rmSync(sourcePath, { force: true });
68
+ }
69
+ });
70
+ };
71
+
72
+ try {
73
+ migrateOne({
74
+ sourcePaths: [managedPaths.legacyHourlyGitSyncPath],
75
+ targetPath: managedPaths.hourlyGitSyncPath,
76
+ });
77
+ migrateOne({
78
+ sourcePaths: [managedPaths.legacyCliDeviceAutoApprovedPath],
79
+ targetPath: managedPaths.cliDeviceAutoApprovedPath,
80
+ });
81
+ } catch (error) {
82
+ logger.error?.(
83
+ `[alphaclaw] Failed to migrate internal managed files: ${error.message || String(error)}`,
84
+ );
85
+ }
86
+
87
+ return managedPaths;
88
+ };
89
+
90
+ module.exports = {
91
+ buildManagedPaths,
92
+ migrateManagedInternalFiles,
93
+ };