@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.
Files changed (68) hide show
  1. package/bin/alphaclaw.js +1 -31
  2. package/lib/public/assets/icons/google_icon.svg +8 -0
  3. package/lib/public/css/explorer.css +53 -0
  4. package/lib/public/css/shell.css +21 -19
  5. package/lib/public/css/theme.css +17 -0
  6. package/lib/public/js/app.js +205 -109
  7. package/lib/public/js/components/credentials-modal.js +36 -8
  8. package/lib/public/js/components/file-tree.js +212 -22
  9. package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
  10. package/lib/public/js/components/file-viewer/index.js +47 -6
  11. package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
  12. package/lib/public/js/components/file-viewer/status-banners.js +11 -6
  13. package/lib/public/js/components/file-viewer/toolbar.js +56 -1
  14. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
  15. package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
  16. package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
  17. package/lib/public/js/components/file-viewer/use-file-viewer.js +142 -15
  18. package/lib/public/js/components/google/account-row.js +131 -0
  19. package/lib/public/js/components/google/add-account-modal.js +93 -0
  20. package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
  21. package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
  22. package/lib/public/js/components/google/index.js +553 -0
  23. package/lib/public/js/components/google/use-gmail-watch.js +140 -0
  24. package/lib/public/js/components/google/use-google-accounts.js +41 -0
  25. package/lib/public/js/components/icons.js +26 -0
  26. package/lib/public/js/components/scope-picker.js +1 -1
  27. package/lib/public/js/components/sidebar-git-panel.js +48 -20
  28. package/lib/public/js/components/sidebar.js +93 -75
  29. package/lib/public/js/components/toast.js +11 -7
  30. package/lib/public/js/components/usage-tab/constants.js +31 -0
  31. package/lib/public/js/components/usage-tab/formatters.js +24 -0
  32. package/lib/public/js/components/usage-tab/index.js +72 -0
  33. package/lib/public/js/components/usage-tab/overview-section.js +147 -0
  34. package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
  35. package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
  36. package/lib/public/js/components/webhooks.js +182 -129
  37. package/lib/public/js/lib/api.js +178 -9
  38. package/lib/public/js/lib/browse-file-policies.js +29 -11
  39. package/lib/public/js/lib/format.js +71 -0
  40. package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
  41. package/lib/public/shared/browse-file-policies.json +13 -0
  42. package/lib/server/constants.js +47 -7
  43. package/lib/server/gmail-push.js +109 -0
  44. package/lib/server/gmail-serve.js +254 -0
  45. package/lib/server/gmail-watch.js +725 -0
  46. package/lib/server/google-state.js +317 -0
  47. package/lib/server/helpers.js +17 -11
  48. package/lib/server/internal-files-migration.js +31 -3
  49. package/lib/server/onboarding/github.js +21 -2
  50. package/lib/server/onboarding/index.js +1 -3
  51. package/lib/server/onboarding/openclaw.js +3 -0
  52. package/lib/server/onboarding/workspace.js +40 -0
  53. package/lib/server/routes/browse/index.js +90 -2
  54. package/lib/server/routes/gmail.js +128 -0
  55. package/lib/server/routes/google.js +433 -213
  56. package/lib/server/routes/system.js +107 -0
  57. package/lib/server/routes/usage.js +29 -2
  58. package/lib/server/routes/webhooks.js +52 -17
  59. package/lib/server/usage-db.js +283 -15
  60. package/lib/server/watchdog.js +66 -0
  61. package/lib/server/webhook-middleware.js +99 -1
  62. package/lib/server/webhooks.js +214 -65
  63. package/lib/server.js +27 -0
  64. package/lib/setup/gitignore +6 -0
  65. package/lib/setup/hourly-git-sync.sh +29 -2
  66. package/package.json +1 -1
  67. package/lib/public/js/components/google.js +0 -228
  68. 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
+ };
@@ -3,7 +3,7 @@ const crypto = require("crypto");
3
3
  const {
4
4
  CODEX_JWT_CLAIM_PATH,
5
5
  kOnboardingModelProviders,
6
- GOG_CREDENTIALS_PATH,
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 c = JSON.parse(fs.readFileSync(GOG_CREDENTIALS_PATH, "utf8"));
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
- c.web?.client_id || c.installed?.client_id || c.client_id || null,
179
- clientSecret:
180
- c.web?.client_secret ||
181
- c.installed?.client_secret ||
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 { clientId: null, clientSecret: null };
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(openclawDir, kHourlyGitSyncFileName),
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) => fs.existsSync(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 (sourcePath !== existingSourcePath && sourcePath !== targetPath && fs.existsSync(sourcePath)) {
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: `Repository "${repoUrl}" already exists.`,
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 };