@chrysb/alphaclaw 0.3.5-beta.1 → 0.4.1-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 +1 -31
- package/lib/public/assets/icons/google_icon.svg +8 -0
- package/lib/public/css/explorer.css +53 -0
- package/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +205 -109
- package/lib/public/js/components/credentials-modal.js +36 -8
- package/lib/public/js/components/file-tree.js +212 -22
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +47 -6
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/status-banners.js +11 -6
- package/lib/public/js/components/file-viewer/toolbar.js +56 -1
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
- package/lib/public/js/components/file-viewer/use-file-viewer.js +142 -15
- package/lib/public/js/components/google/account-row.js +131 -0
- package/lib/public/js/components/google/add-account-modal.js +93 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
- package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
- package/lib/public/js/components/google/index.js +553 -0
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/google/use-google-accounts.js +41 -0
- package/lib/public/js/components/icons.js +26 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +48 -20
- package/lib/public/js/components/sidebar.js +93 -75
- package/lib/public/js/components/toast.js +11 -7
- package/lib/public/js/components/usage-tab/constants.js +31 -0
- package/lib/public/js/components/usage-tab/formatters.js +24 -0
- package/lib/public/js/components/usage-tab/index.js +72 -0
- package/lib/public/js/components/usage-tab/overview-section.js +147 -0
- package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
- package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
- package/lib/public/js/components/webhooks.js +182 -129
- package/lib/public/js/lib/api.js +178 -9
- package/lib/public/js/lib/browse-file-policies.js +29 -11
- package/lib/public/js/lib/format.js +71 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
- package/lib/public/shared/browse-file-policies.json +13 -0
- package/lib/server/constants.js +47 -7
- package/lib/server/gmail-push.js +109 -0
- package/lib/server/gmail-serve.js +254 -0
- package/lib/server/gmail-watch.js +725 -0
- package/lib/server/google-state.js +317 -0
- package/lib/server/helpers.js +17 -11
- package/lib/server/internal-files-migration.js +31 -3
- package/lib/server/onboarding/github.js +21 -2
- package/lib/server/onboarding/index.js +1 -3
- package/lib/server/onboarding/openclaw.js +3 -0
- package/lib/server/onboarding/workspace.js +40 -0
- package/lib/server/routes/browse/index.js +90 -2
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +433 -213
- package/lib/server/routes/system.js +107 -0
- package/lib/server/routes/usage.js +29 -2
- package/lib/server/routes/webhooks.js +52 -17
- package/lib/server/usage-db.js +283 -15
- package/lib/server/watchdog.js +66 -0
- package/lib/server/webhook-middleware.js +99 -1
- package/lib/server/webhooks.js +214 -65
- package/lib/server.js +27 -0
- package/lib/setup/gitignore +6 -0
- package/lib/setup/hourly-git-sync.sh +29 -2
- package/package.json +1 -1
- package/lib/public/js/components/google.js +0 -228
- package/lib/public/js/components/usage-tab.js +0 -531
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
const kGoogleStateVersion = 2;
|
|
4
|
+
const kDefaultGoogleClient = "default";
|
|
5
|
+
const kDefaultGoogleScopes = [
|
|
6
|
+
"gmail:read",
|
|
7
|
+
"calendar:read",
|
|
8
|
+
"calendar:write",
|
|
9
|
+
"drive:read",
|
|
10
|
+
"sheets:read",
|
|
11
|
+
"docs:read",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const createEmptyGoogleState = () => ({
|
|
15
|
+
version: kGoogleStateVersion,
|
|
16
|
+
accounts: [],
|
|
17
|
+
gmailPush: {
|
|
18
|
+
token: "",
|
|
19
|
+
topics: {},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const createGoogleAccountId = () => crypto.randomBytes(4).toString("hex");
|
|
24
|
+
|
|
25
|
+
const normalizeScopes = (services) => {
|
|
26
|
+
if (!Array.isArray(services)) return [...kDefaultGoogleScopes];
|
|
27
|
+
const deduped = Array.from(
|
|
28
|
+
new Set(
|
|
29
|
+
services
|
|
30
|
+
.map((scope) => String(scope || "").trim())
|
|
31
|
+
.filter(Boolean),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
return deduped.length ? deduped : [...kDefaultGoogleScopes];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const normalizePositiveInt = (value, fallbackValue = null) => {
|
|
38
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
39
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
40
|
+
return fallbackValue;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const normalizeTimestamp = (value) => {
|
|
44
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
45
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const normalizeGmailWatch = (gmailWatch = {}) => {
|
|
50
|
+
const enabled = Boolean(gmailWatch?.enabled);
|
|
51
|
+
return {
|
|
52
|
+
enabled,
|
|
53
|
+
port: enabled ? normalizePositiveInt(gmailWatch?.port) : null,
|
|
54
|
+
expiration: normalizeTimestamp(gmailWatch?.expiration),
|
|
55
|
+
lastPushAt: normalizeTimestamp(gmailWatch?.lastPushAt),
|
|
56
|
+
pid: enabled ? normalizePositiveInt(gmailWatch?.pid) : null,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const normalizeGmailPush = (gmailPush = {}) => {
|
|
61
|
+
const rawTopics = gmailPush?.topics;
|
|
62
|
+
const topics = Object.fromEntries(
|
|
63
|
+
Object.entries(rawTopics && typeof rawTopics === "object" ? rawTopics : {})
|
|
64
|
+
.map(([client, topic]) => [
|
|
65
|
+
String(client || "").trim(),
|
|
66
|
+
String(topic || "").trim(),
|
|
67
|
+
])
|
|
68
|
+
.filter(([client, topic]) => client && topic),
|
|
69
|
+
);
|
|
70
|
+
return {
|
|
71
|
+
token: String(gmailPush?.token || "").trim(),
|
|
72
|
+
topics,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const isLikelyPersonalEmail = (email = "") => {
|
|
77
|
+
const normalized = String(email || "").trim().toLowerCase();
|
|
78
|
+
return normalized.endsWith("@gmail.com") || normalized.endsWith("@googlemail.com");
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const normalizePersonalFlag = ({ account = {}, client = kDefaultGoogleClient }) => {
|
|
82
|
+
if (typeof account.personal === "boolean") return account.personal;
|
|
83
|
+
if (client === "personal") return true;
|
|
84
|
+
return isLikelyPersonalEmail(account.email);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const normalizeGoogleAccount = (account = {}) => ({
|
|
88
|
+
// Backward-compatible migration path for older state entries that predate
|
|
89
|
+
// explicit personal flags or were saved before the personal marker existed.
|
|
90
|
+
...(() => {
|
|
91
|
+
const client =
|
|
92
|
+
String(account.client || kDefaultGoogleClient).trim() || kDefaultGoogleClient;
|
|
93
|
+
return {
|
|
94
|
+
id: String(account.id || createGoogleAccountId()),
|
|
95
|
+
email: String(account.email || "").trim(),
|
|
96
|
+
client,
|
|
97
|
+
personal: normalizePersonalFlag({ account, client }),
|
|
98
|
+
services: normalizeScopes(account.services),
|
|
99
|
+
authenticated: Boolean(account.authenticated),
|
|
100
|
+
gmailWatch: normalizeGmailWatch(account.gmailWatch),
|
|
101
|
+
};
|
|
102
|
+
})(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const normalizeGoogleStateV2 = (state = {}) => {
|
|
106
|
+
const accounts = Array.isArray(state.accounts)
|
|
107
|
+
? state.accounts.map((account) => normalizeGoogleAccount(account))
|
|
108
|
+
: [];
|
|
109
|
+
return {
|
|
110
|
+
version: kGoogleStateVersion,
|
|
111
|
+
accounts,
|
|
112
|
+
gmailPush: normalizeGmailPush(state.gmailPush),
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const hasPersonalGoogleAccount = (state = {}) =>
|
|
117
|
+
(state.accounts || []).some((account) => account.personal);
|
|
118
|
+
|
|
119
|
+
const writeGoogleState = ({ fs, statePath, state }) => {
|
|
120
|
+
const normalized = normalizeGoogleStateV2(state);
|
|
121
|
+
fs.writeFileSync(statePath, JSON.stringify(normalized, null, 2));
|
|
122
|
+
return normalized;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const migrateGoogleStateV1 = ({ fs, statePath, rawState = {} }) => {
|
|
126
|
+
const email = String(rawState.email || "").trim();
|
|
127
|
+
const accounts = email
|
|
128
|
+
? [
|
|
129
|
+
normalizeGoogleAccount({
|
|
130
|
+
id: createGoogleAccountId(),
|
|
131
|
+
email,
|
|
132
|
+
services: rawState.services,
|
|
133
|
+
authenticated: Boolean(rawState.authenticated),
|
|
134
|
+
client: kDefaultGoogleClient,
|
|
135
|
+
personal: false,
|
|
136
|
+
}),
|
|
137
|
+
]
|
|
138
|
+
: [];
|
|
139
|
+
const migrated = {
|
|
140
|
+
version: kGoogleStateVersion,
|
|
141
|
+
accounts,
|
|
142
|
+
gmailPush: normalizeGmailPush({}),
|
|
143
|
+
};
|
|
144
|
+
fs.writeFileSync(statePath, JSON.stringify(migrated, null, 2));
|
|
145
|
+
return migrated;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const readGoogleState = ({ fs, statePath }) => {
|
|
149
|
+
if (!fs.existsSync(statePath)) return createEmptyGoogleState();
|
|
150
|
+
try {
|
|
151
|
+
const raw = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
152
|
+
if (raw && raw.version === kGoogleStateVersion && Array.isArray(raw.accounts)) {
|
|
153
|
+
const normalized = normalizeGoogleStateV2(raw);
|
|
154
|
+
if (JSON.stringify(raw) !== JSON.stringify(normalized)) {
|
|
155
|
+
fs.writeFileSync(statePath, JSON.stringify(normalized, null, 2));
|
|
156
|
+
}
|
|
157
|
+
return normalized;
|
|
158
|
+
}
|
|
159
|
+
return migrateGoogleStateV1({ fs, statePath, rawState: raw || {} });
|
|
160
|
+
} catch {
|
|
161
|
+
return createEmptyGoogleState();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const listGoogleAccounts = (state = {}) => [...(state.accounts || [])];
|
|
166
|
+
|
|
167
|
+
const getGoogleAccountById = (state = {}, accountId = "") =>
|
|
168
|
+
(state.accounts || []).find((account) => account.id === accountId) || null;
|
|
169
|
+
|
|
170
|
+
const getGoogleAccountByEmailAndClient = (
|
|
171
|
+
state = {},
|
|
172
|
+
email = "",
|
|
173
|
+
client = kDefaultGoogleClient,
|
|
174
|
+
) =>
|
|
175
|
+
(state.accounts || []).find(
|
|
176
|
+
(account) => account.email === email && account.client === client,
|
|
177
|
+
) || null;
|
|
178
|
+
|
|
179
|
+
const getGoogleAccountByEmail = (state = {}, email = "") => {
|
|
180
|
+
const normalizedEmail = String(email || "").trim().toLowerCase();
|
|
181
|
+
if (!normalizedEmail) return null;
|
|
182
|
+
return (
|
|
183
|
+
(state.accounts || []).find(
|
|
184
|
+
(account) =>
|
|
185
|
+
String(account.email || "").trim().toLowerCase() === normalizedEmail,
|
|
186
|
+
) || null
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const getGmailPushConfig = (state = {}) => normalizeGmailPush(state.gmailPush);
|
|
191
|
+
|
|
192
|
+
const setGmailPushConfig = ({ state = {}, config = {} }) => {
|
|
193
|
+
const nextState = normalizeGoogleStateV2(state);
|
|
194
|
+
nextState.gmailPush = normalizeGmailPush({
|
|
195
|
+
...nextState.gmailPush,
|
|
196
|
+
...config,
|
|
197
|
+
topics: {
|
|
198
|
+
...(nextState.gmailPush?.topics || {}),
|
|
199
|
+
...((config?.topics && typeof config.topics === "object")
|
|
200
|
+
? config.topics
|
|
201
|
+
: {}),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
return { state: nextState, gmailPush: nextState.gmailPush };
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const getAccountGmailWatch = (account = {}) =>
|
|
208
|
+
normalizeGmailWatch(account?.gmailWatch);
|
|
209
|
+
|
|
210
|
+
const setAccountGmailWatch = ({ state = {}, accountId = "", watch = {} }) => {
|
|
211
|
+
const nextState = normalizeGoogleStateV2(state);
|
|
212
|
+
const targetId = String(accountId || "").trim();
|
|
213
|
+
if (!targetId) return { state: nextState, account: null };
|
|
214
|
+
const accountIndex = nextState.accounts.findIndex(
|
|
215
|
+
(account) => account.id === targetId,
|
|
216
|
+
);
|
|
217
|
+
if (accountIndex === -1) return { state: nextState, account: null };
|
|
218
|
+
const account = nextState.accounts[accountIndex];
|
|
219
|
+
const mergedWatch = normalizeGmailWatch({
|
|
220
|
+
...(account.gmailWatch || {}),
|
|
221
|
+
...watch,
|
|
222
|
+
});
|
|
223
|
+
const nextAccount = {
|
|
224
|
+
...account,
|
|
225
|
+
gmailWatch: mergedWatch,
|
|
226
|
+
};
|
|
227
|
+
nextState.accounts[accountIndex] = nextAccount;
|
|
228
|
+
return { state: nextState, account: nextAccount };
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const listWatchEnabledAccounts = (state = {}) =>
|
|
232
|
+
(state.accounts || []).filter((account) =>
|
|
233
|
+
Boolean(normalizeGmailWatch(account.gmailWatch).enabled),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const generatePushToken = () => crypto.randomBytes(24).toString("base64url");
|
|
237
|
+
|
|
238
|
+
const allocateServePort = ({
|
|
239
|
+
state = {},
|
|
240
|
+
basePort = 18801,
|
|
241
|
+
maxAccounts = 5,
|
|
242
|
+
}) => {
|
|
243
|
+
const usedPorts = new Set(
|
|
244
|
+
(state.accounts || [])
|
|
245
|
+
.map((account) => normalizePositiveInt(account?.gmailWatch?.port))
|
|
246
|
+
.filter(Boolean),
|
|
247
|
+
);
|
|
248
|
+
for (let offset = 0; offset < maxAccounts; offset += 1) {
|
|
249
|
+
const candidate = basePort + offset;
|
|
250
|
+
if (!usedPorts.has(candidate)) return candidate;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const upsertGoogleAccount = ({
|
|
256
|
+
state,
|
|
257
|
+
account,
|
|
258
|
+
maxAccounts = 5,
|
|
259
|
+
}) => {
|
|
260
|
+
const nextState = normalizeGoogleStateV2(state);
|
|
261
|
+
const normalized = normalizeGoogleAccount(account);
|
|
262
|
+
if (!normalized.email) throw new Error("Account email is required");
|
|
263
|
+
const existingIdx = nextState.accounts.findIndex((item) => item.id === normalized.id);
|
|
264
|
+
|
|
265
|
+
if (normalized.personal) {
|
|
266
|
+
const personalExists = nextState.accounts.some(
|
|
267
|
+
(item, idx) => item.personal && idx !== existingIdx,
|
|
268
|
+
);
|
|
269
|
+
if (personalExists) {
|
|
270
|
+
throw new Error("Only one personal account is allowed");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (existingIdx >= 0) {
|
|
275
|
+
nextState.accounts[existingIdx] = normalized;
|
|
276
|
+
return { state: nextState, account: normalized };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (nextState.accounts.length >= maxAccounts) {
|
|
280
|
+
throw new Error(`Maximum ${maxAccounts} Google accounts allowed`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
nextState.accounts.push(normalized);
|
|
284
|
+
return { state: nextState, account: normalized };
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const removeGoogleAccount = ({ state, accountId }) => {
|
|
288
|
+
const nextState = normalizeGoogleStateV2(state);
|
|
289
|
+
const removed = getGoogleAccountById(nextState, accountId);
|
|
290
|
+
if (!removed) return { state: nextState, account: null };
|
|
291
|
+
nextState.accounts = nextState.accounts.filter((account) => account.id !== accountId);
|
|
292
|
+
return { state: nextState, account: removed };
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
module.exports = {
|
|
296
|
+
kGoogleStateVersion,
|
|
297
|
+
kDefaultGoogleClient,
|
|
298
|
+
kDefaultGoogleScopes,
|
|
299
|
+
createGoogleAccountId,
|
|
300
|
+
createEmptyGoogleState,
|
|
301
|
+
readGoogleState,
|
|
302
|
+
writeGoogleState,
|
|
303
|
+
listGoogleAccounts,
|
|
304
|
+
getGoogleAccountById,
|
|
305
|
+
getGoogleAccountByEmailAndClient,
|
|
306
|
+
getGoogleAccountByEmail,
|
|
307
|
+
upsertGoogleAccount,
|
|
308
|
+
removeGoogleAccount,
|
|
309
|
+
hasPersonalGoogleAccount,
|
|
310
|
+
getGmailPushConfig,
|
|
311
|
+
setGmailPushConfig,
|
|
312
|
+
getAccountGmailWatch,
|
|
313
|
+
setAccountGmailWatch,
|
|
314
|
+
listWatchEnabledAccounts,
|
|
315
|
+
generatePushToken,
|
|
316
|
+
allocateServePort,
|
|
317
|
+
};
|
package/lib/server/helpers.js
CHANGED
|
@@ -3,7 +3,7 @@ const crypto = require("crypto");
|
|
|
3
3
|
const {
|
|
4
4
|
CODEX_JWT_CLAIM_PATH,
|
|
5
5
|
kOnboardingModelProviders,
|
|
6
|
-
|
|
6
|
+
gogClientCredentialsPath,
|
|
7
7
|
} = require("./constants");
|
|
8
8
|
|
|
9
9
|
const normalizeOpenclawVersion = (rawVersion) => {
|
|
@@ -170,20 +170,26 @@ const getApiEnableUrl = (svc, projectId) => {
|
|
|
170
170
|
return `https://console.developers.google.com/apis/api/${api}/overview${project}`;
|
|
171
171
|
};
|
|
172
172
|
|
|
173
|
-
const readGoogleCredentials = () => {
|
|
173
|
+
const readGoogleCredentials = (clientName = "default") => {
|
|
174
174
|
try {
|
|
175
|
-
const
|
|
175
|
+
const credentialsPath = gogClientCredentialsPath(clientName);
|
|
176
|
+
const c = JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
|
|
177
|
+
const webCredentials = c.web || c.installed || c;
|
|
176
178
|
return {
|
|
177
|
-
clientId:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
c.client_secret ||
|
|
183
|
-
null,
|
|
179
|
+
clientId: webCredentials?.client_id || null,
|
|
180
|
+
clientSecret: webCredentials?.client_secret || null,
|
|
181
|
+
projectId: webCredentials?.project_id || null,
|
|
182
|
+
path: credentialsPath,
|
|
183
|
+
client: clientName,
|
|
184
184
|
};
|
|
185
185
|
} catch {
|
|
186
|
-
return {
|
|
186
|
+
return {
|
|
187
|
+
clientId: null,
|
|
188
|
+
clientSecret: null,
|
|
189
|
+
projectId: null,
|
|
190
|
+
path: gogClientCredentialsPath(clientName),
|
|
191
|
+
client: clientName,
|
|
192
|
+
};
|
|
187
193
|
}
|
|
188
194
|
};
|
|
189
195
|
|
|
@@ -3,6 +3,11 @@ const path = require("path");
|
|
|
3
3
|
const kInternalDirName = ".alphaclaw";
|
|
4
4
|
const kHourlyGitSyncFileName = "hourly-git-sync.sh";
|
|
5
5
|
const kCliDeviceAutoApprovedFileName = ".cli-device-auto-approved";
|
|
6
|
+
const kOpenclawGitignoreHookEntries = [
|
|
7
|
+
"!hooks/",
|
|
8
|
+
"!hooks/transforms/",
|
|
9
|
+
"!hooks/transforms/**",
|
|
10
|
+
];
|
|
6
11
|
|
|
7
12
|
const buildManagedPaths = ({ openclawDir, pathModule = path }) => {
|
|
8
13
|
const internalDir = pathModule.join(openclawDir, kInternalDirName);
|
|
@@ -13,7 +18,10 @@ const buildManagedPaths = ({ openclawDir, pathModule = path }) => {
|
|
|
13
18
|
internalDir,
|
|
14
19
|
kCliDeviceAutoApprovedFileName,
|
|
15
20
|
),
|
|
16
|
-
legacyHourlyGitSyncPath: pathModule.join(
|
|
21
|
+
legacyHourlyGitSyncPath: pathModule.join(
|
|
22
|
+
openclawDir,
|
|
23
|
+
kHourlyGitSyncFileName,
|
|
24
|
+
),
|
|
17
25
|
legacyCliDeviceAutoApprovedPath: pathModule.join(
|
|
18
26
|
openclawDir,
|
|
19
27
|
kCliDeviceAutoApprovedFileName,
|
|
@@ -45,7 +53,9 @@ const migrateManagedInternalFiles = ({
|
|
|
45
53
|
fs.mkdirSync(managedPaths.internalDir, { recursive: true });
|
|
46
54
|
|
|
47
55
|
const migrateOne = ({ sourcePaths, targetPath }) => {
|
|
48
|
-
const existingSourcePath = sourcePaths.find((sourcePath) =>
|
|
56
|
+
const existingSourcePath = sourcePaths.find((sourcePath) =>
|
|
57
|
+
fs.existsSync(sourcePath),
|
|
58
|
+
);
|
|
49
59
|
if (fs.existsSync(targetPath)) {
|
|
50
60
|
sourcePaths.forEach((sourcePath) => {
|
|
51
61
|
if (sourcePath !== targetPath && fs.existsSync(sourcePath)) {
|
|
@@ -63,13 +73,31 @@ const migrateManagedInternalFiles = ({
|
|
|
63
73
|
mode: sourceStats.mode,
|
|
64
74
|
});
|
|
65
75
|
sourcePaths.forEach((sourcePath) => {
|
|
66
|
-
if (
|
|
76
|
+
if (
|
|
77
|
+
sourcePath !== existingSourcePath &&
|
|
78
|
+
sourcePath !== targetPath &&
|
|
79
|
+
fs.existsSync(sourcePath)
|
|
80
|
+
) {
|
|
67
81
|
fs.rmSync(sourcePath, { force: true });
|
|
68
82
|
}
|
|
69
83
|
});
|
|
70
84
|
};
|
|
71
85
|
|
|
72
86
|
try {
|
|
87
|
+
const gitignorePath = pathModule.join(openclawDir, ".gitignore");
|
|
88
|
+
if (fs.existsSync(gitignorePath)) {
|
|
89
|
+
const raw = String(fs.readFileSync(gitignorePath, "utf8") || "");
|
|
90
|
+
const existingLines = raw.split(/\r?\n/);
|
|
91
|
+
const existingSet = new Set(existingLines.map((line) => line.trim()));
|
|
92
|
+
const missing = kOpenclawGitignoreHookEntries.filter(
|
|
93
|
+
(line) => !existingSet.has(line),
|
|
94
|
+
);
|
|
95
|
+
if (missing.length) {
|
|
96
|
+
const separator = raw.endsWith("\n") || !raw.length ? "" : "\n";
|
|
97
|
+
const next = `${raw}${separator}${missing.join("\n")}\n`;
|
|
98
|
+
fs.writeFileSync(gitignorePath, next);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
73
101
|
migrateOne({
|
|
74
102
|
sourcePaths: [managedPaths.legacyHourlyGitSyncPath],
|
|
75
103
|
targetPath: managedPaths.hourlyGitSyncPath,
|
|
@@ -64,13 +64,28 @@ const verifyGithubRepoForOnboarding = async ({ repoUrl, githubToken }) => {
|
|
|
64
64
|
headers: ghHeaders,
|
|
65
65
|
});
|
|
66
66
|
if (checkRes.status === 404) {
|
|
67
|
-
return { ok: true };
|
|
67
|
+
return { ok: true, repoExists: false, repoIsEmpty: false };
|
|
68
68
|
}
|
|
69
69
|
if (checkRes.ok) {
|
|
70
|
+
const commitsRes = await fetch(
|
|
71
|
+
`https://api.github.com/repos/${repoUrl}/commits?per_page=1`,
|
|
72
|
+
{ headers: ghHeaders },
|
|
73
|
+
);
|
|
74
|
+
if (commitsRes.status === 409) {
|
|
75
|
+
return { ok: true, repoExists: true, repoIsEmpty: true };
|
|
76
|
+
}
|
|
77
|
+
if (commitsRes.ok) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
status: 400,
|
|
81
|
+
error: `Repository "${repoUrl}" already exists and is not empty.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const commitCheckDetails = await parseGithubErrorMessage(commitsRes);
|
|
70
85
|
return {
|
|
71
86
|
ok: false,
|
|
72
87
|
status: 400,
|
|
73
|
-
error: `
|
|
88
|
+
error: `Cannot verify whether repo "${repoUrl}" is empty: ${commitCheckDetails}`,
|
|
74
89
|
};
|
|
75
90
|
}
|
|
76
91
|
|
|
@@ -100,6 +115,10 @@ const ensureGithubRepoAccessible = async ({
|
|
|
100
115
|
githubToken,
|
|
101
116
|
});
|
|
102
117
|
if (!verification.ok) return verification;
|
|
118
|
+
if (verification.repoExists && verification.repoIsEmpty) {
|
|
119
|
+
console.log(`[onboard] Using existing empty repo ${repoUrl}`);
|
|
120
|
+
return { ok: true };
|
|
121
|
+
}
|
|
103
122
|
|
|
104
123
|
try {
|
|
105
124
|
console.log(`[onboard] Creating repo ${repoUrl}...`);
|
|
@@ -17,9 +17,7 @@ const {
|
|
|
17
17
|
installHourlyGitSyncScript,
|
|
18
18
|
installHourlyGitSyncCron,
|
|
19
19
|
} = require("./cron");
|
|
20
|
-
const {
|
|
21
|
-
migrateManagedInternalFiles,
|
|
22
|
-
} = require("../internal-files-migration");
|
|
20
|
+
const { migrateManagedInternalFiles } = require("../internal-files-migration");
|
|
23
21
|
|
|
24
22
|
const createOnboardingService = ({
|
|
25
23
|
fs,
|
|
@@ -8,6 +8,7 @@ const kUsageTrackerPluginPath = path.resolve(
|
|
|
8
8
|
"plugin",
|
|
9
9
|
"usage-tracker",
|
|
10
10
|
);
|
|
11
|
+
const kDefaultToolsProfile = "full";
|
|
11
12
|
|
|
12
13
|
const buildOnboardArgs = ({
|
|
13
14
|
varMap,
|
|
@@ -134,10 +135,12 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
|
|
|
134
135
|
if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
|
|
135
136
|
if (!cfg.plugins.entries) cfg.plugins.entries = {};
|
|
136
137
|
if (!cfg.commands) cfg.commands = {};
|
|
138
|
+
if (!cfg.tools) cfg.tools = {};
|
|
137
139
|
if (!cfg.hooks) cfg.hooks = {};
|
|
138
140
|
if (!cfg.hooks.internal) cfg.hooks.internal = {};
|
|
139
141
|
if (!cfg.hooks.internal.entries) cfg.hooks.internal.entries = {};
|
|
140
142
|
cfg.commands.restart = true;
|
|
143
|
+
cfg.tools.profile = kDefaultToolsProfile;
|
|
141
144
|
cfg.hooks.internal.enabled = true;
|
|
142
145
|
cfg.hooks.internal.entries["bootstrap-extra-files"] = {
|
|
143
146
|
...(cfg.hooks.internal.entries["bootstrap-extra-files"] || {}),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const { kSetupDir, OPENCLAW_DIR } = require("../constants");
|
|
3
3
|
const { renderTopicRegistryMarkdown } = require("../topic-registry");
|
|
4
|
+
const { readGoogleState } = require("../google-state");
|
|
4
5
|
|
|
5
6
|
const resolveSetupUiUrl = (baseUrl) => {
|
|
6
7
|
const normalizedBaseUrl = String(baseUrl || "").trim().replace(/\/+$/, "");
|
|
@@ -32,6 +33,41 @@ const isTelegramWorkspaceEnabled = (fs) => {
|
|
|
32
33
|
}
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
const renderGoogleAccountsMarkdown = (fs) => {
|
|
37
|
+
try {
|
|
38
|
+
const googleStatePath = `${OPENCLAW_DIR}/gogcli/state.json`;
|
|
39
|
+
const state = readGoogleState({ fs, statePath: googleStatePath });
|
|
40
|
+
const accounts = Array.isArray(state.accounts) ? state.accounts : [];
|
|
41
|
+
let section = "\n\n## Available Google Accounts\n\n";
|
|
42
|
+
if (!accounts.length) {
|
|
43
|
+
section += "No Google accounts are currently configured.\n";
|
|
44
|
+
return section;
|
|
45
|
+
}
|
|
46
|
+
section +=
|
|
47
|
+
"Configured in AlphaClaw (use `--client <client> --account <email>` for gog commands):\n\n";
|
|
48
|
+
section += accounts
|
|
49
|
+
.map((account) => {
|
|
50
|
+
const email = String(account.email || "").trim() || "(unknown email)";
|
|
51
|
+
const client = String(account.client || "default").trim() || "default";
|
|
52
|
+
const personal = account.personal ? "personal" : "company";
|
|
53
|
+
const auth = account.authenticated ? "authenticated" : "awaiting sign-in";
|
|
54
|
+
const services = Array.isArray(account.services) ? account.services.join(", ") : "";
|
|
55
|
+
const metaParts = [
|
|
56
|
+
`type: ${personal}`,
|
|
57
|
+
`client: ${client}`,
|
|
58
|
+
`status: ${auth}`,
|
|
59
|
+
services ? `services: ${services}` : null,
|
|
60
|
+
].filter(Boolean);
|
|
61
|
+
return `- ${email} (${metaParts.join("; ")})`;
|
|
62
|
+
})
|
|
63
|
+
.join("\n");
|
|
64
|
+
section += "\n";
|
|
65
|
+
return section;
|
|
66
|
+
} catch {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
35
71
|
const syncBootstrapPromptFiles = ({ fs, workspaceDir, baseUrl }) => {
|
|
36
72
|
try {
|
|
37
73
|
const setupUiUrl = resolveSetupUiUrl(baseUrl);
|
|
@@ -51,6 +87,10 @@ const syncBootstrapPromptFiles = ({ fs, workspaceDir, baseUrl }) => {
|
|
|
51
87
|
if (topicSection) {
|
|
52
88
|
toolsContent += topicSection;
|
|
53
89
|
}
|
|
90
|
+
const googleAccountsSection = renderGoogleAccountsMarkdown(fs);
|
|
91
|
+
if (googleAccountsSection) {
|
|
92
|
+
toolsContent += googleAccountsSection;
|
|
93
|
+
}
|
|
54
94
|
|
|
55
95
|
fs.writeFileSync(`${bootstrapDir}/TOOLS.md`, toolsContent);
|
|
56
96
|
console.log("[onboard] Bootstrap prompt files synced");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
|
-
const { kLockedBrowsePaths } = require("../../constants");
|
|
2
|
+
const { kLockedBrowsePaths, kProtectedBrowsePaths } = require("../../constants");
|
|
3
3
|
const {
|
|
4
4
|
kDefaultTreeDepth,
|
|
5
5
|
kIgnoredDirectoryNames,
|
|
@@ -30,7 +30,6 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
30
30
|
const kRootResolved = path.resolve(kRootDir);
|
|
31
31
|
const kRootWithSep = `${kRootResolved}${path.sep}`;
|
|
32
32
|
const kRootDisplayName = "kRootDir/.openclaw";
|
|
33
|
-
|
|
34
33
|
if (!fs.existsSync(kRootResolved)) {
|
|
35
34
|
fs.mkdirSync(kRootResolved, { recursive: true });
|
|
36
35
|
}
|
|
@@ -369,7 +368,14 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
369
368
|
.split("\n")
|
|
370
369
|
.map((line) => line.trim())
|
|
371
370
|
.filter(Boolean);
|
|
371
|
+
const rawStatus = statusLines[0]?.slice(0, 2) || "";
|
|
372
372
|
const isUntracked = statusLines.some((line) => line.startsWith("??"));
|
|
373
|
+
const statusKind =
|
|
374
|
+
rawStatus === "??" || rawStatus.includes("A")
|
|
375
|
+
? "U"
|
|
376
|
+
: rawStatus.includes("D")
|
|
377
|
+
? "D"
|
|
378
|
+
: "M";
|
|
373
379
|
|
|
374
380
|
const diffResult = isUntracked
|
|
375
381
|
? await runGitCommandWithExitCode(
|
|
@@ -397,6 +403,8 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
397
403
|
ok: true,
|
|
398
404
|
path: relativePath,
|
|
399
405
|
content,
|
|
406
|
+
statusKind,
|
|
407
|
+
isDeleted: statusKind === "D",
|
|
400
408
|
});
|
|
401
409
|
} catch (error) {
|
|
402
410
|
return res.status(500).json({
|
|
@@ -567,6 +575,86 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
567
575
|
.json({ ok: false, error: error.message || "Could not save file" });
|
|
568
576
|
}
|
|
569
577
|
});
|
|
578
|
+
|
|
579
|
+
app.delete("/api/browse/delete", (req, res) => {
|
|
580
|
+
const targetPath = String(req.body?.path || "").trim();
|
|
581
|
+
const resolvedPath = resolveSafePath(
|
|
582
|
+
targetPath,
|
|
583
|
+
kRootResolved,
|
|
584
|
+
kRootWithSep,
|
|
585
|
+
kRootDisplayName,
|
|
586
|
+
);
|
|
587
|
+
if (!resolvedPath.ok) {
|
|
588
|
+
return res.status(400).json({ ok: false, error: resolvedPath.error });
|
|
589
|
+
}
|
|
590
|
+
const normalizedPolicyPath = normalizePolicyPath(resolvedPath.relativePath);
|
|
591
|
+
if (
|
|
592
|
+
matchesPolicyPath(kLockedBrowsePaths, normalizedPolicyPath) ||
|
|
593
|
+
matchesPolicyPath(kProtectedBrowsePaths, normalizedPolicyPath)
|
|
594
|
+
) {
|
|
595
|
+
return res.status(403).json({
|
|
596
|
+
ok: false,
|
|
597
|
+
error: "This file cannot be deleted from the explorer.",
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
if (!fs.existsSync(resolvedPath.absolutePath)) {
|
|
602
|
+
return res.status(404).json({ ok: false, error: "File does not exist" });
|
|
603
|
+
}
|
|
604
|
+
const stats = fs.statSync(resolvedPath.absolutePath);
|
|
605
|
+
if (!stats.isFile()) {
|
|
606
|
+
return res.status(400).json({ ok: false, error: "Path is not a file" });
|
|
607
|
+
}
|
|
608
|
+
fs.rmSync(resolvedPath.absolutePath, { force: true });
|
|
609
|
+
return res.json({
|
|
610
|
+
ok: true,
|
|
611
|
+
path: resolvedPath.relativePath,
|
|
612
|
+
});
|
|
613
|
+
} catch (error) {
|
|
614
|
+
return res.status(500).json({
|
|
615
|
+
ok: false,
|
|
616
|
+
error: error.message || "Could not delete file",
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
app.post("/api/browse/restore", async (req, res) => {
|
|
622
|
+
const { path: targetPath } = req.body || {};
|
|
623
|
+
const resolvedPath = resolveSafePath(
|
|
624
|
+
targetPath,
|
|
625
|
+
kRootResolved,
|
|
626
|
+
kRootWithSep,
|
|
627
|
+
kRootDisplayName,
|
|
628
|
+
);
|
|
629
|
+
if (!resolvedPath.ok) {
|
|
630
|
+
return res.status(400).json({ ok: false, error: resolvedPath.error });
|
|
631
|
+
}
|
|
632
|
+
const relativePath = String(resolvedPath.relativePath || "").trim();
|
|
633
|
+
if (!relativePath) {
|
|
634
|
+
return res.status(400).json({ ok: false, error: "path is required" });
|
|
635
|
+
}
|
|
636
|
+
const restoreResult = await runGitCommand(
|
|
637
|
+
["restore", "--staged", "--worktree", "--", relativePath],
|
|
638
|
+
kRootResolved,
|
|
639
|
+
);
|
|
640
|
+
const fallbackResult = !restoreResult.ok
|
|
641
|
+
? await runGitCommand(["checkout", "--", relativePath], kRootResolved)
|
|
642
|
+
: { ok: true };
|
|
643
|
+
if (!restoreResult.ok && !fallbackResult.ok) {
|
|
644
|
+
return res.status(500).json({
|
|
645
|
+
ok: false,
|
|
646
|
+
error:
|
|
647
|
+
restoreResult.error ||
|
|
648
|
+
fallbackResult.error ||
|
|
649
|
+
"Could not restore file from git",
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return res.json({
|
|
653
|
+
ok: true,
|
|
654
|
+
path: relativePath,
|
|
655
|
+
restored: fs.existsSync(resolvedPath.absolutePath),
|
|
656
|
+
});
|
|
657
|
+
});
|
|
570
658
|
};
|
|
571
659
|
|
|
572
660
|
module.exports = { registerBrowseRoutes };
|