@chrysb/alphaclaw 0.4.0 → 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 (47) hide show
  1. package/lib/public/css/shell.css +21 -19
  2. package/lib/public/css/theme.css +17 -0
  3. package/lib/public/js/app.js +80 -5
  4. package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
  5. package/lib/public/js/components/file-viewer/index.js +3 -0
  6. package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
  7. package/lib/public/js/components/file-viewer/toolbar.js +13 -0
  8. package/lib/public/js/components/file-viewer/use-file-viewer.js +48 -13
  9. package/lib/public/js/components/google/account-row.js +34 -1
  10. package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
  11. package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
  12. package/lib/public/js/components/google/index.js +118 -4
  13. package/lib/public/js/components/google/use-gmail-watch.js +140 -0
  14. package/lib/public/js/components/scope-picker.js +1 -1
  15. package/lib/public/js/components/sidebar-git-panel.js +5 -6
  16. package/lib/public/js/components/sidebar.js +2 -0
  17. package/lib/public/js/components/toast.js +11 -7
  18. package/lib/public/js/components/usage-tab/constants.js +31 -0
  19. package/lib/public/js/components/usage-tab/formatters.js +24 -0
  20. package/lib/public/js/components/usage-tab/index.js +72 -0
  21. package/lib/public/js/components/usage-tab/overview-section.js +147 -0
  22. package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
  23. package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
  24. package/lib/public/js/components/webhooks.js +182 -129
  25. package/lib/public/js/lib/api.js +106 -1
  26. package/lib/public/js/lib/format.js +71 -0
  27. package/lib/server/constants.js +28 -0
  28. package/lib/server/gmail-push.js +109 -0
  29. package/lib/server/gmail-serve.js +254 -0
  30. package/lib/server/gmail-watch.js +725 -0
  31. package/lib/server/google-state.js +130 -0
  32. package/lib/server/helpers.js +5 -7
  33. package/lib/server/internal-files-migration.js +31 -3
  34. package/lib/server/routes/gmail.js +128 -0
  35. package/lib/server/routes/google.js +19 -0
  36. package/lib/server/routes/system.js +107 -0
  37. package/lib/server/routes/usage.js +29 -2
  38. package/lib/server/routes/webhooks.js +52 -17
  39. package/lib/server/usage-db.js +283 -15
  40. package/lib/server/watchdog.js +66 -0
  41. package/lib/server/webhook-middleware.js +99 -1
  42. package/lib/server/webhooks.js +214 -65
  43. package/lib/server.js +27 -0
  44. package/lib/setup/gitignore +3 -0
  45. package/lib/setup/hourly-git-sync.sh +1 -1
  46. package/package.json +1 -1
  47. package/lib/public/js/components/usage-tab.js +0 -531
@@ -3,6 +3,8 @@ const https = require("https");
3
3
  const { URL } = require("url");
4
4
 
5
5
  const kRedactedHeaderKeys = new Set(["authorization", "cookie", "x-webhook-token"]);
6
+ const kGmailDedupeTtlMs = 24 * 60 * 60 * 1000;
7
+ const kGmailDedupeCleanupIntervalMs = 60 * 1000;
6
8
 
7
9
  const normalizeIp = (ip) => String(ip || "").replace(/^::ffff:/, "");
8
10
 
@@ -74,6 +76,51 @@ const resolveGatewayPath = ({ pathname, search }) => {
74
76
  return `${pathname}${search || ""}`;
75
77
  };
76
78
 
79
+ const parseJsonSafe = (rawValue) => {
80
+ try {
81
+ return JSON.parse(String(rawValue || "").trim() || "{}");
82
+ } catch {
83
+ return null;
84
+ }
85
+ };
86
+
87
+ const getGmailPayloadData = (parsedBody) => {
88
+ if (!parsedBody || typeof parsedBody !== "object") return null;
89
+ if (parsedBody.payload && typeof parsedBody.payload === "object") {
90
+ return parsedBody.payload;
91
+ }
92
+ return parsedBody;
93
+ };
94
+
95
+ const getGmailMessageId = (message = {}) => {
96
+ const preferredId = String(message?.id || "").trim();
97
+ if (preferredId) return preferredId;
98
+ const fallbackId = String(message?.messageId || "").trim();
99
+ return fallbackId;
100
+ };
101
+
102
+ const buildGmailDedupedBodyBuffer = ({ parsedBody, filteredMessages }) => {
103
+ if (parsedBody?.payload && typeof parsedBody.payload === "object") {
104
+ return Buffer.from(
105
+ JSON.stringify({
106
+ ...parsedBody,
107
+ payload: {
108
+ ...parsedBody.payload,
109
+ messages: filteredMessages,
110
+ },
111
+ }),
112
+ "utf8",
113
+ );
114
+ }
115
+ return Buffer.from(
116
+ JSON.stringify({
117
+ ...(parsedBody || {}),
118
+ messages: filteredMessages,
119
+ }),
120
+ "utf8",
121
+ );
122
+ };
123
+
77
124
  const createWebhookMiddleware = ({
78
125
  gatewayUrl,
79
126
  insertRequest,
@@ -81,6 +128,18 @@ const createWebhookMiddleware = ({
81
128
  }) => {
82
129
  const gateway = new URL(gatewayUrl);
83
130
  const protocolClient = gateway.protocol === "https:" ? https : http;
131
+ const gmailSeenMessageIds = new Map();
132
+ let lastGmailDedupeCleanupAt = 0;
133
+
134
+ const pruneGmailSeenMessageIds = (nowMs) => {
135
+ if (nowMs - lastGmailDedupeCleanupAt < kGmailDedupeCleanupIntervalMs) return;
136
+ for (const [messageKey, seenAt] of gmailSeenMessageIds.entries()) {
137
+ if (nowMs - seenAt > kGmailDedupeTtlMs) {
138
+ gmailSeenMessageIds.delete(messageKey);
139
+ }
140
+ }
141
+ lastGmailDedupeCleanupAt = nowMs;
142
+ };
84
143
 
85
144
  return (req, res) => {
86
145
  const inboundUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
@@ -90,8 +149,47 @@ const createWebhookMiddleware = ({
90
149
  inboundUrl.searchParams.delete("token");
91
150
  }
92
151
 
93
- const bodyBuffer = extractBodyBuffer(req);
152
+ let bodyBuffer = extractBodyBuffer(req);
94
153
  const hookName = resolveHookName(req);
154
+
155
+ if (hookName === "gmail" && bodyBuffer.length > 0) {
156
+ const parsedBody = parseJsonSafe(bodyBuffer.toString("utf8"));
157
+ const payloadData = getGmailPayloadData(parsedBody);
158
+ const accountKey = String(
159
+ payloadData?.account || payloadData?.email || payloadData?.inbox || "unknown",
160
+ )
161
+ .trim()
162
+ .toLowerCase();
163
+ const messages = Array.isArray(payloadData?.messages) ? payloadData.messages : [];
164
+ if (messages.length > 0) {
165
+ const nowMs = Date.now();
166
+ pruneGmailSeenMessageIds(nowMs);
167
+ const unseenMessages = [];
168
+ for (const message of messages) {
169
+ const messageId = getGmailMessageId(message);
170
+ if (!messageId) {
171
+ unseenMessages.push(message);
172
+ continue;
173
+ }
174
+ const dedupeKey = `${accountKey}:${messageId}`;
175
+ if (gmailSeenMessageIds.has(dedupeKey)) {
176
+ continue;
177
+ }
178
+ gmailSeenMessageIds.set(dedupeKey, nowMs);
179
+ unseenMessages.push(message);
180
+ }
181
+ if (unseenMessages.length === 0) {
182
+ return res.status(200).json({ ok: true, deduped: true });
183
+ }
184
+ if (unseenMessages.length < messages.length && parsedBody) {
185
+ bodyBuffer = buildGmailDedupedBodyBuffer({
186
+ parsedBody,
187
+ filteredMessages: unseenMessages,
188
+ });
189
+ }
190
+ }
191
+ }
192
+
95
193
  const sourceIp = normalizeIp(
96
194
  req.ip || req.headers["x-forwarded-for"] || req.socket?.remoteAddress || "",
97
195
  );
@@ -2,6 +2,14 @@ const path = require("path");
2
2
 
3
3
  const kNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4
4
  const kTransformsDir = "hooks/transforms";
5
+ const kManagedWebhookConfigs = [
6
+ {
7
+ name: "gmail",
8
+ preset: "gmail",
9
+ description:
10
+ "Managed by AlphaClaw Gmail Watch setup. Required for internal Gmail watch delivery.",
11
+ },
12
+ ];
5
13
 
6
14
  const getConfigPath = ({ OPENCLAW_DIR }) =>
7
15
  path.join(OPENCLAW_DIR, "openclaw.json");
@@ -45,7 +53,25 @@ const ensureHooksRoot = (cfg) => {
45
53
  if (typeof cfg.hooks.path !== "string" || !cfg.hooks.path.trim())
46
54
  cfg.hooks.path = "/hooks";
47
55
  if (typeof cfg.hooks.token !== "string" || !cfg.hooks.token.trim()) {
48
- cfg.hooks.token = "${WEBHOOK_TOKEN}";
56
+ cfg.hooks.token = "${OPENCLAW_HOOKS_TOKEN}";
57
+ }
58
+ if (
59
+ typeof cfg.hooks.defaultSessionKey !== "string" ||
60
+ !cfg.hooks.defaultSessionKey.trim()
61
+ ) {
62
+ cfg.hooks.defaultSessionKey = "hook:ingress";
63
+ }
64
+ if (typeof cfg.hooks.allowRequestSessionKey !== "boolean") {
65
+ cfg.hooks.allowRequestSessionKey = false;
66
+ }
67
+ if (!Array.isArray(cfg.hooks.allowedSessionKeyPrefixes)) {
68
+ cfg.hooks.allowedSessionKeyPrefixes = ["hook:"];
69
+ }
70
+ if (!cfg.hooks.allowedSessionKeyPrefixes.includes("hook:")) {
71
+ cfg.hooks.allowedSessionKeyPrefixes = [
72
+ ...cfg.hooks.allowedSessionKeyPrefixes,
73
+ "hook:",
74
+ ];
49
75
  }
50
76
  return cfg.hooks.mappings;
51
77
  };
@@ -100,36 +126,155 @@ const normalizeMappingTransformModules = (mappings) => {
100
126
  return changed;
101
127
  };
102
128
 
129
+ const buildDefaultTransformSource = (name) =>
130
+ [
131
+ "export default async function transform(payload, context) {",
132
+ " const data = payload.payload || payload;",
133
+ " return {",
134
+ " message: data.message,",
135
+ ` name: data.name || "${name}",`,
136
+ ' wakeMode: data.wakeMode || "now",',
137
+ " };",
138
+ "}",
139
+ "",
140
+ ].join("\n");
141
+
142
+ const ensureWebhookTransform = ({ fs, constants, name, source = "" }) => {
143
+ const webhookName = validateWebhookName(name);
144
+ const transformAbsolutePath = getTransformAbsolutePath(
145
+ constants,
146
+ webhookName,
147
+ );
148
+ fs.mkdirSync(path.dirname(transformAbsolutePath), { recursive: true });
149
+ if (fs.existsSync(transformAbsolutePath)) {
150
+ return { changed: false, path: transformAbsolutePath };
151
+ }
152
+ fs.writeFileSync(
153
+ transformAbsolutePath,
154
+ String(source || "").trim()
155
+ ? `${String(source).replace(/\s+$/, "")}\n`
156
+ : buildDefaultTransformSource(webhookName),
157
+ );
158
+ return { changed: true, path: transformAbsolutePath };
159
+ };
160
+
161
+ const ensureWebhookMapping = ({ cfg, name, mapping = {} }) => {
162
+ const webhookName = validateWebhookName(name);
163
+ const mappings = ensureHooksRoot(cfg);
164
+ const normalizedModulesChanged = normalizeMappingTransformModules(mappings);
165
+ const index = findMappingIndexByName(mappings, webhookName);
166
+ const defaults = {
167
+ match: { path: webhookName },
168
+ action: "agent",
169
+ name: webhookName,
170
+ wakeMode: "now",
171
+ transform: { module: getTransformModulePath(webhookName) },
172
+ };
173
+ if (index === -1) {
174
+ mappings.push({
175
+ ...defaults,
176
+ ...mapping,
177
+ match: { ...defaults.match, ...(mapping.match || {}) },
178
+ transform: { ...defaults.transform, ...(mapping.transform || {}) },
179
+ });
180
+ return { changed: true, created: true, normalizedModulesChanged };
181
+ }
182
+ const current = mappings[index] || {};
183
+ const next = {
184
+ ...current,
185
+ ...mapping,
186
+ match: {
187
+ ...(current.match || {}),
188
+ ...(mapping.match || {}),
189
+ path: webhookName,
190
+ },
191
+ action: mapping.action || current.action || defaults.action,
192
+ wakeMode: mapping.wakeMode || current.wakeMode || defaults.wakeMode,
193
+ transform: {
194
+ ...(current.transform || {}),
195
+ ...(mapping.transform || {}),
196
+ module:
197
+ String(mapping?.transform?.module || "").trim() ||
198
+ String(current?.transform?.module || "").trim() ||
199
+ defaults.transform.module,
200
+ },
201
+ };
202
+ if (JSON.stringify(current) !== JSON.stringify(next)) {
203
+ mappings[index] = next;
204
+ return { changed: true, created: false, normalizedModulesChanged };
205
+ }
206
+ return {
207
+ changed: normalizedModulesChanged,
208
+ created: false,
209
+ normalizedModulesChanged,
210
+ };
211
+ };
212
+
213
+ const listManagedWebhooksFromConfig = ({ cfg }) => {
214
+ const presets = Array.isArray(cfg?.hooks?.presets) ? cfg.hooks.presets : [];
215
+ return kManagedWebhookConfigs
216
+ .filter((managed) => presets.includes(managed.preset))
217
+ .map((managed) => ({
218
+ name: managed.name,
219
+ enabled: true,
220
+ createdAt: null,
221
+ path: `/hooks/${managed.name}`,
222
+ transformPath: null,
223
+ transformExists: true,
224
+ managed: true,
225
+ managedReason: managed.description,
226
+ }));
227
+ };
228
+
229
+ const isManagedWebhook = ({ cfg, name }) => {
230
+ const normalized = String(name || "")
231
+ .trim()
232
+ .toLowerCase();
233
+ if (!normalized) return false;
234
+ return listManagedWebhooksFromConfig({ cfg }).some(
235
+ (webhook) => webhook.name === normalized,
236
+ );
237
+ };
238
+
103
239
  const listWebhooks = ({ fs, constants }) => {
104
240
  const { cfg } = readConfig({ fs, constants });
105
241
  const mappings = ensureHooksRoot(cfg);
106
- return mappings
107
- .filter(isWebhookMapping)
108
- .map((mapping) => {
109
- const name = getMappingHookName(mapping);
110
- const transformPath = resolveTransformPathFromMapping(name, mapping);
111
- const transformAbsolutePath = path.join(
112
- constants.OPENCLAW_DIR,
113
- transformPath,
114
- );
115
- let createdAt = null;
116
- try {
117
- const stat = fs.statSync(transformAbsolutePath);
118
- createdAt =
119
- stat.birthtime?.toISOString?.() ||
120
- stat.ctime?.toISOString?.() ||
121
- null;
122
- } catch {}
123
- return {
124
- name,
125
- enabled: true,
126
- createdAt,
127
- path: `/hooks/${name}`,
128
- transformPath,
129
- transformExists: fs.existsSync(transformAbsolutePath),
130
- };
131
- })
132
- .sort((a, b) => a.name.localeCompare(b.name));
242
+ const managedWebhooks = listManagedWebhooksFromConfig({ cfg });
243
+ const managedByName = new Map(
244
+ managedWebhooks.map((item) => [item.name, item]),
245
+ );
246
+ const mappingWebhooks = mappings.filter(isWebhookMapping).map((mapping) => {
247
+ const name = getMappingHookName(mapping);
248
+ const managed = managedByName.get(name);
249
+ const transformPath = resolveTransformPathFromMapping(name, mapping);
250
+ const transformAbsolutePath = path.join(
251
+ constants.OPENCLAW_DIR,
252
+ transformPath,
253
+ );
254
+ let createdAt = null;
255
+ try {
256
+ const stat = fs.statSync(transformAbsolutePath);
257
+ createdAt =
258
+ stat.birthtime?.toISOString?.() || stat.ctime?.toISOString?.() || null;
259
+ } catch {}
260
+ return {
261
+ name,
262
+ enabled: true,
263
+ createdAt,
264
+ path: `/hooks/${name}`,
265
+ transformPath,
266
+ transformExists: fs.existsSync(transformAbsolutePath),
267
+ managed: Boolean(managed),
268
+ managedReason: managed?.managedReason || "",
269
+ };
270
+ });
271
+ const mappingNames = new Set(mappingWebhooks.map((item) => item.name));
272
+ const syntheticManagedWebhooks = managedWebhooks.filter(
273
+ (item) => !mappingNames.has(item.name),
274
+ );
275
+ return [...mappingWebhooks, ...syntheticManagedWebhooks].sort((a, b) =>
276
+ a.name.localeCompare(b.name),
277
+ );
133
278
  };
134
279
 
135
280
  const getWebhookDetail = ({ fs, constants, name }) => {
@@ -137,6 +282,12 @@ const getWebhookDetail = ({ fs, constants, name }) => {
137
282
  const hooks = listWebhooks({ fs, constants });
138
283
  const detail = hooks.find((item) => item.name === webhookName);
139
284
  if (!detail) return null;
285
+ if (detail.managed || !detail.transformPath) {
286
+ return {
287
+ ...detail,
288
+ transformExists: true,
289
+ };
290
+ }
140
291
  const transformAbsolutePath = path.join(
141
292
  constants.OPENCLAW_DIR,
142
293
  detail.transformPath,
@@ -147,56 +298,54 @@ const getWebhookDetail = ({ fs, constants, name }) => {
147
298
  };
148
299
  };
149
300
 
150
- const ensureStarterTransform = ({ fs, constants, name }) => {
151
- const transformAbsolutePath = getTransformAbsolutePath(constants, name);
152
- fs.mkdirSync(path.dirname(transformAbsolutePath), { recursive: true });
153
- if (fs.existsSync(transformAbsolutePath)) return transformAbsolutePath;
154
- fs.writeFileSync(
155
- transformAbsolutePath,
156
- [
157
- "export default async function transform(payload, context) {",
158
- " const data = payload.payload || payload;",
159
- " return {",
160
- " message: data.message,",
161
- ` name: data.name || "${name}",`,
162
- ' wakeMode: data.wakeMode || "now",',
163
- " };",
164
- "}",
165
- "",
166
- ].join("\n"),
167
- );
168
- return transformAbsolutePath;
169
- };
170
-
171
- const createWebhook = ({ fs, constants, name }) => {
301
+ const createWebhook = ({
302
+ fs,
303
+ constants,
304
+ name,
305
+ upsert = false,
306
+ allowManagedName = false,
307
+ mapping = {},
308
+ transformSource = "",
309
+ }) => {
172
310
  const webhookName = validateWebhookName(name);
173
311
  const { cfg, configPath } = readConfig({ fs, constants });
174
- if (!cfg.hooks) cfg.hooks = {};
175
- const mappings = ensureHooksRoot(cfg);
176
- const normalizedModules = normalizeMappingTransformModules(mappings);
177
- if (findMappingIndexByName(mappings, webhookName) !== -1) {
312
+ if (!allowManagedName && isManagedWebhook({ cfg, name: webhookName })) {
313
+ throw new Error(
314
+ `Webhook "${webhookName}" is managed and cannot be created manually`,
315
+ );
316
+ }
317
+ const existingMappings = ensureHooksRoot(cfg);
318
+ const exists = findMappingIndexByName(existingMappings, webhookName) !== -1;
319
+ if (exists && !upsert) {
178
320
  throw new Error(`Webhook "${webhookName}" already exists`);
179
321
  }
180
- mappings.push({
181
- match: { path: webhookName },
182
- action: "agent",
322
+ const ensuredMapping = ensureWebhookMapping({
323
+ cfg,
183
324
  name: webhookName,
184
- wakeMode: "now",
185
- transform: { module: getTransformModulePath(webhookName) },
325
+ mapping,
186
326
  });
187
-
188
- if (normalizedModules) {
189
- // Keep all existing mappings consistent with transformsDir-relative module paths.
190
- cfg.hooks.mappings = mappings;
327
+ const ensuredTransform = ensureWebhookTransform({
328
+ fs,
329
+ constants,
330
+ name: webhookName,
331
+ source: transformSource,
332
+ });
333
+ if (ensuredMapping.changed || ensuredTransform.changed || !exists) {
334
+ writeConfig({ fs, configPath, cfg });
191
335
  }
192
- writeConfig({ fs, configPath, cfg });
193
- ensureStarterTransform({ fs, constants, name: webhookName });
194
336
  return getWebhookDetail({ fs, constants, name: webhookName });
195
337
  };
196
338
 
197
339
  const deleteWebhook = ({ fs, constants, name, deleteTransformDir = false }) => {
198
340
  const webhookName = validateWebhookName(name);
199
341
  const { cfg, configPath } = readConfig({ fs, constants });
342
+ if (isManagedWebhook({ cfg, name: webhookName })) {
343
+ return {
344
+ removed: false,
345
+ managed: true,
346
+ deletedTransformDir: false,
347
+ };
348
+ }
200
349
  const mappings = ensureHooksRoot(cfg);
201
350
  const normalizedModules = normalizeMappingTransformModules(mappings);
202
351
  const index = findMappingIndexByName(mappings, webhookName);
package/lib/server.js CHANGED
@@ -98,6 +98,7 @@ const { registerTelegramRoutes } = require("./server/routes/telegram");
98
98
  const { registerWebhookRoutes } = require("./server/routes/webhooks");
99
99
  const { registerWatchdogRoutes } = require("./server/routes/watchdog");
100
100
  const { registerUsageRoutes } = require("./server/routes/usage");
101
+ const { registerGmailRoutes } = require("./server/routes/gmail");
101
102
 
102
103
  const { PORT, GATEWAY_URL, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
103
104
 
@@ -111,6 +112,7 @@ migrateManagedInternalFiles({
111
112
  const app = express();
112
113
  app.set("trust proxy", kTrustProxyHops);
113
114
  app.use(["/webhook", "/hooks"], express.raw({ type: "*/*", limit: "5mb" }));
115
+ app.use("/gmail-pubsub", express.raw({ type: "*/*", limit: "5mb" }));
114
116
  app.use(express.json());
115
117
 
116
118
  const proxy = httpProxy.createProxyServer({
@@ -241,6 +243,18 @@ registerGoogleRoutes({
241
243
  getApiEnableUrl,
242
244
  constants,
243
245
  });
246
+ const gmailWatchService = registerGmailRoutes({
247
+ app,
248
+ fs,
249
+ constants,
250
+ gogCmd,
251
+ getBaseUrl,
252
+ readGoogleCredentials,
253
+ readEnvFile,
254
+ writeEnvFile,
255
+ reloadEnv,
256
+ restartRequiredState,
257
+ });
244
258
  const telegramApi = createTelegramApi(() => process.env.TELEGRAM_BOT_TOKEN);
245
259
  const discordApi = createDiscordApi(() => process.env.DISCORD_BOT_TOKEN);
246
260
  const watchdogNotifier = createWatchdogNotifier({ telegramApi, discordApi });
@@ -345,7 +359,20 @@ server.listen(PORT, "0.0.0.0", () => {
345
359
  ensureGatewayProxyConfig(null);
346
360
  startGateway();
347
361
  watchdog.start();
362
+ gmailWatchService.start();
348
363
  } else {
349
364
  console.log("[alphaclaw] Awaiting onboarding via Setup UI");
350
365
  }
351
366
  });
367
+
368
+ const shutdownGmailWatchService = async () => {
369
+ try {
370
+ await gmailWatchService.stop();
371
+ } catch {}
372
+ };
373
+ process.on("SIGTERM", () => {
374
+ shutdownGmailWatchService();
375
+ });
376
+ process.on("SIGINT", () => {
377
+ shutdownGmailWatchService();
378
+ });
@@ -13,6 +13,9 @@ db/
13
13
  db/**
14
14
  !skills/
15
15
  !skills/**
16
+ !hooks/
17
+ !hooks/transforms/
18
+ !hooks/transforms/**
16
19
  !cron/
17
20
  !cron/jobs.json
18
21
  !openclaw.json
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- REPO="$(cd "$(dirname "$0")" && pwd)"
4
+ REPO="$(cd "$(dirname "$0")/.." && pwd)"
5
5
  cd "$REPO"
6
6
 
7
7
  # Load persisted env vars when running under cron's minimal environment.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.4.0",
3
+ "version": "0.4.1-beta.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },