@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.
- package/bin/alphaclaw.js +82 -3
- package/lib/public/css/explorer.css +385 -9
- package/lib/public/css/theme.css +1 -1
- package/lib/public/js/app.js +102 -8
- package/lib/public/js/components/channels.js +1 -0
- package/lib/public/js/components/file-tree.js +74 -38
- package/lib/public/js/components/file-viewer/constants.js +6 -0
- package/lib/public/js/components/file-viewer/diff-viewer.js +46 -0
- package/lib/public/js/components/file-viewer/editor-surface.js +120 -0
- package/lib/public/js/components/file-viewer/frontmatter-panel.js +56 -0
- package/lib/public/js/components/file-viewer/index.js +164 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +51 -0
- package/lib/public/js/components/file-viewer/media-preview.js +44 -0
- package/lib/public/js/components/file-viewer/scroll-sync.js +95 -0
- package/lib/public/js/components/file-viewer/sqlite-viewer.js +167 -0
- package/lib/public/js/components/file-viewer/status-banners.js +59 -0
- package/lib/public/js/components/file-viewer/storage.js +58 -0
- package/lib/public/js/components/file-viewer/toolbar.js +77 -0
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +87 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +49 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +302 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-draft-sync.js +32 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-hotkeys.js +25 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +379 -0
- package/lib/public/js/components/file-viewer/utils.js +11 -0
- package/lib/public/js/components/gateway.js +95 -48
- package/lib/public/js/components/icons.js +26 -0
- package/lib/public/js/components/sidebar-git-panel.js +219 -31
- package/lib/public/js/components/sidebar.js +1 -1
- package/lib/public/js/components/usage-tab.js +4 -1
- package/lib/public/js/components/watchdog-tab.js +6 -2
- package/lib/public/js/lib/api.js +31 -0
- package/lib/public/js/lib/browse-file-policies.js +34 -0
- package/lib/scripts/git +40 -0
- package/lib/scripts/git-askpass +6 -0
- package/lib/server/constants.js +8 -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/constants.js +51 -0
- package/lib/server/routes/browse/file-helpers.js +43 -0
- package/lib/server/routes/browse/git.js +131 -0
- package/lib/server/routes/browse/index.js +572 -0
- package/lib/server/routes/browse/path-utils.js +53 -0
- package/lib/server/routes/browse/sqlite.js +140 -0
- package/lib/server/routes/pairings.js +8 -2
- package/lib/server/routes/proxy.js +11 -5
- package/lib/server/routes/system.js +5 -1
- package/lib/server.js +7 -0
- package/lib/setup/core-prompts/TOOLS.md +0 -4
- package/package.json +1 -1
- package/lib/public/js/components/file-viewer.js +0 -864
- 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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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"
|
|
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
|
${(() => {
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -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
|
+
};
|
package/lib/scripts/git
ADDED
|
@@ -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" "$@"
|
package/lib/server/constants.js
CHANGED
|
@@ -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,
|
package/lib/server/helpers.js
CHANGED
|
@@ -8,7 +8,11 @@ const {
|
|
|
8
8
|
|
|
9
9
|
const normalizeOpenclawVersion = (rawVersion) => {
|
|
10
10
|
if (!rawVersion) return null;
|
|
11
|
-
return
|
|
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(
|
|
69
|
+
["1", "true", "yes", "on"].includes(
|
|
70
|
+
String(value || "")
|
|
71
|
+
.trim()
|
|
72
|
+
.toLowerCase(),
|
|
73
|
+
);
|
|
66
74
|
const isDebugEnabled = () =>
|
|
67
|
-
isTruthyEnvFlag(process.env.ALPHACLAW_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:
|
|
177
|
+
clientId:
|
|
178
|
+
c.web?.client_id || c.installed?.client_id || c.client_id || null,
|
|
169
179
|
clientSecret:
|
|
170
|
-
c.web?.client_secret ||
|
|
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
|
+
};
|