visionclaw 0.1.182-beta.5 → 0.1.182-beta.6

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.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Unified billing handler for both subscription (`payment_required`) and
3
+ * pay-as-you-go (`request_payment`) heartbeat commands.
4
+ *
5
+ * This is deterministic bot code — the LLM agent is never involved in
6
+ * payment flows. The handler:
7
+ *
8
+ * 1. Receives a billing command from the backend via heartbeat
9
+ * 2. Sends a selection prompt (inline keyboard) to the owner via Telegram
10
+ * 3. Listens for selection events from the Telegram adapter
11
+ * 4. Calls the backend to create a Stripe Checkout / Payment Link
12
+ * 5. Sends the payment URL back to the owner as a URL button
13
+ */
14
+ import type { CommandHandler } from "../agent/command-dispatcher.js";
15
+ import type { ChannelManager } from "../channels/manager.js";
16
+ import type { BillingPlanSelectedEvent, BillingPaygSelectedEvent } from "../channels/interface.js";
17
+ import type { OnboardingServiceClient } from "../onboarding/onboarding-service-client.js";
18
+ /**
19
+ * BillingHandler manages both subscription and PAYG payment flows.
20
+ * Created once at startup; event listeners should be wired externally
21
+ * (in HeartbeatManager) to avoid duplicate registrations.
22
+ */
23
+ export declare class BillingHandler {
24
+ private readonly channelManager;
25
+ private readonly ownerChatId;
26
+ private readonly serviceClient;
27
+ constructor(channelManager: ChannelManager, ownerChatId: number, serviceClient: OnboardingServiceClient | null);
28
+ /**
29
+ * CommandHandler for the `payment_required` heartbeat command.
30
+ * Parses the payload and sends a plan selection prompt.
31
+ */
32
+ readonly handlePaymentRequired: CommandHandler;
33
+ /**
34
+ * Handle a `billing_plan_selected` event (user tapped a plan button).
35
+ * Creates a Stripe Checkout Session and sends the link.
36
+ */
37
+ handlePlanSelected(event: BillingPlanSelectedEvent): Promise<void>;
38
+ /**
39
+ * CommandHandler for the `request_payment` heartbeat command.
40
+ * Parses the payload and sends an amount selection prompt.
41
+ */
42
+ readonly handleRequestPayment: CommandHandler;
43
+ /**
44
+ * Handle a `billing_payg_selected` event (user tapped an amount button).
45
+ * Creates a one-time Stripe Payment Link and sends the URL.
46
+ */
47
+ handlePaygSelected(event: BillingPaygSelectedEvent): Promise<void>;
48
+ /**
49
+ * Send a Telegram prompt (plan picker or amount picker) and return
50
+ * a command result.
51
+ */
52
+ private sendPrompt;
53
+ /**
54
+ * Create a Stripe checkout / payment link with retries, then send
55
+ * the resulting URL to the owner via Telegram.
56
+ */
57
+ private createAndSendLink;
58
+ }
59
+ //# sourceMappingURL=billing-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing-handler.d.ts","sourceRoot":"","sources":["../../src/billing/billing-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACnG,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AAW1F;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAiC;gBAG7D,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,uBAAuB,GAAG,IAAI;IAS/C;;;OAGG;IACH,QAAQ,CAAC,qBAAqB,EAAE,cAAc,CA8B5C;IAEF;;;OAGG;IACG,kBAAkB,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWxE;;;OAGG;IACH,QAAQ,CAAC,oBAAoB,EAAE,cAAc,CA4B3C;IAEF;;;OAGG;IACG,kBAAkB,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWxE;;;OAGG;YACW,UAAU;IAoBxB;;;OAGG;YACW,iBAAiB;CA0DhC"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Unified billing handler for both subscription (`payment_required`) and
3
+ * pay-as-you-go (`request_payment`) heartbeat commands.
4
+ *
5
+ * This is deterministic bot code — the LLM agent is never involved in
6
+ * payment flows. The handler:
7
+ *
8
+ * 1. Receives a billing command from the backend via heartbeat
9
+ * 2. Sends a selection prompt (inline keyboard) to the owner via Telegram
10
+ * 3. Listens for selection events from the Telegram adapter
11
+ * 4. Calls the backend to create a Stripe Checkout / Payment Link
12
+ * 5. Sends the payment URL back to the owner as a URL button
13
+ */
14
+ import { logger } from "../logger.js";
15
+ const MAX_RETRIES = 2;
16
+ const RETRY_DELAY_MS = 2_000;
17
+ function sleep(ms) {
18
+ return new Promise((resolve) => setTimeout(resolve, ms));
19
+ }
20
+ /**
21
+ * BillingHandler manages both subscription and PAYG payment flows.
22
+ * Created once at startup; event listeners should be wired externally
23
+ * (in HeartbeatManager) to avoid duplicate registrations.
24
+ */
25
+ export class BillingHandler {
26
+ channelManager;
27
+ ownerChatId;
28
+ serviceClient;
29
+ constructor(channelManager, ownerChatId, serviceClient) {
30
+ this.channelManager = channelManager;
31
+ this.ownerChatId = ownerChatId;
32
+ this.serviceClient = serviceClient;
33
+ }
34
+ // ── Subscription flow ──────────────────────────────────────────────
35
+ /**
36
+ * CommandHandler for the `payment_required` heartbeat command.
37
+ * Parses the payload and sends a plan selection prompt.
38
+ */
39
+ handlePaymentRequired = async (_commandId, payload) => {
40
+ const raw = (payload ?? {});
41
+ const plans = Array.isArray(raw.plans)
42
+ ? raw.plans
43
+ : [];
44
+ if (plans.length === 0) {
45
+ logger.warn("[billing] payment_required received with no plans");
46
+ return {
47
+ status: "failed",
48
+ result: JSON.stringify({ error: "No plans in payload" }),
49
+ };
50
+ }
51
+ const typedPayload = {
52
+ message: typeof raw.message === "string" ? raw.message : undefined,
53
+ plans,
54
+ };
55
+ logger.system(`[billing] Payment required received with ${typedPayload.plans.length} plan(s)`);
56
+ return this.sendPrompt(() => this.channelManager.sendTelegramPaymentPrompt(this.ownerChatId, typedPayload), "billing");
57
+ };
58
+ /**
59
+ * Handle a `billing_plan_selected` event (user tapped a plan button).
60
+ * Creates a Stripe Checkout Session and sends the link.
61
+ */
62
+ async handlePlanSelected(event) {
63
+ await this.createAndSendLink(event.chatId, "billing", event.planName, (client) => client.createCheckoutSession(event.planId, event.chatId));
64
+ }
65
+ // ── PAYG flow ──────────────────────────────────────────────────────
66
+ /**
67
+ * CommandHandler for the `request_payment` heartbeat command.
68
+ * Parses the payload and sends an amount selection prompt.
69
+ */
70
+ handleRequestPayment = async (_commandId, payload) => {
71
+ const raw = (payload ?? {});
72
+ const amounts = Array.isArray(raw.amounts) ? raw.amounts : [];
73
+ if (amounts.length === 0) {
74
+ logger.warn("[payg] request_payment received with no amounts");
75
+ return {
76
+ status: "failed",
77
+ result: JSON.stringify({ error: "No amounts in payload" }),
78
+ };
79
+ }
80
+ const typedPayload = {
81
+ reason: typeof raw.reason === "string" ? raw.reason : undefined,
82
+ amounts,
83
+ };
84
+ logger.system(`[payg] Request payment received with amounts: ${typedPayload.amounts.join(", ")}`);
85
+ return this.sendPrompt(() => this.channelManager.sendTelegramPaygPrompt(this.ownerChatId, typedPayload), "payg");
86
+ };
87
+ /**
88
+ * Handle a `billing_payg_selected` event (user tapped an amount button).
89
+ * Creates a one-time Stripe Payment Link and sends the URL.
90
+ */
91
+ async handlePaygSelected(event) {
92
+ await this.createAndSendLink(event.chatId, "payg", `$${event.amount} Top-Up`, (client) => client.createPaymentLink(event.amount, event.chatId));
93
+ }
94
+ // ── Shared helpers ─────────────────────────────────────────────────
95
+ /**
96
+ * Send a Telegram prompt (plan picker or amount picker) and return
97
+ * a command result.
98
+ */
99
+ async sendPrompt(send, tag) {
100
+ try {
101
+ await send();
102
+ return {
103
+ status: "completed",
104
+ result: JSON.stringify({ message: `${tag} prompt sent to owner` }),
105
+ };
106
+ }
107
+ catch (err) {
108
+ const errMsg = err instanceof Error ? err.message : String(err);
109
+ logger.err(`[${tag}] Failed to send prompt: ${errMsg}`);
110
+ return {
111
+ status: "failed",
112
+ result: JSON.stringify({ error: errMsg }),
113
+ };
114
+ }
115
+ }
116
+ /**
117
+ * Create a Stripe checkout / payment link with retries, then send
118
+ * the resulting URL to the owner via Telegram.
119
+ */
120
+ async createAndSendLink(chatId, tag, linkLabel, createLink) {
121
+ if (!this.serviceClient) {
122
+ logger.warn(`[${tag}] No service client — cannot create payment link`);
123
+ await this.channelManager.sendMessage("telegram", String(chatId), "Payment is not configured. Please contact support.");
124
+ return;
125
+ }
126
+ const client = this.serviceClient;
127
+ logger.system(`[${tag}] Selection received from chat ${chatId}`);
128
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
129
+ try {
130
+ const result = await createLink(client);
131
+ if (result?.checkoutUrl) {
132
+ await this.channelManager.sendTelegramCheckoutLink(chatId, linkLabel, result.checkoutUrl);
133
+ logger.system(`[${tag}] Payment link sent to chat ${chatId}`);
134
+ return;
135
+ }
136
+ if (attempt < MAX_RETRIES) {
137
+ logger.warn(`[${tag}] Payment link attempt ${attempt + 1} failed, retrying...`);
138
+ await this.channelManager.sendMessage("telegram", String(chatId), "Payment setup failed, retrying...");
139
+ await sleep(RETRY_DELAY_MS);
140
+ }
141
+ }
142
+ catch (err) {
143
+ const errMsg = err instanceof Error ? err.message : String(err);
144
+ logger.err(`[${tag}] Payment link attempt ${attempt + 1} error: ${errMsg}`);
145
+ if (attempt < MAX_RETRIES) {
146
+ await sleep(RETRY_DELAY_MS);
147
+ }
148
+ }
149
+ }
150
+ logger.err(`[${tag}] All payment link attempts failed`);
151
+ await this.channelManager.sendMessage("telegram", String(chatId), "Payment setup failed. Please try again later.");
152
+ }
153
+ }
154
+ //# sourceMappingURL=billing-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing-handler.js","sourceRoot":"","sources":["../../src/billing/billing-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * PAYG (Pay as You Go) command handler for the `request_payment` heartbeat command.
3
+ *
4
+ * This is deterministic bot code u2014 the LLM agent is never involved in
5
+ * payment flows. The handler:
6
+ *
7
+ * 1. Receives `request_payment` from the backend via heartbeat (includes amounts)
8
+ * 2. Sends an amount selection prompt (inline keyboard) to the owner via Telegram
9
+ * 3. Listens for `billing_payg_selected` events from the Telegram adapter
10
+ * 4. Calls the backend to create a Stripe Payment Link (one-time)
11
+ * 5. Sends the payment URL back to the owner as a URL button
12
+ */
13
+ import type { CommandHandler } from "../agent/command-dispatcher.js";
14
+ import type { ChannelManager } from "../channels/manager.js";
15
+ import type { BillingPaygSelectedEvent } from "../channels/interface.js";
16
+ import type { OnboardingServiceClient } from "../onboarding/onboarding-service-client.js";
17
+ export interface PaygHandler extends CommandHandler {
18
+ /** Handle a billing_payg_selected event (user tapped an amount button). */
19
+ handleAmountSelected: (event: BillingPaygSelectedEvent) => Promise<void>;
20
+ }
21
+ /**
22
+ * Create a CommandHandler for the "request_payment" heartbeat command.
23
+ *
24
+ * @param channelManager - To send PAYG prompts and listen for amount selection events
25
+ * @param ownerChatId - The owner's Telegram chat ID
26
+ * @param serviceClient - Onboarding service client for creating payment links
27
+ */
28
+ export declare function createPaygHandler(channelManager: ChannelManager, ownerChatId: number, serviceClient: OnboardingServiceClient | null): PaygHandler;
29
+ //# sourceMappingURL=payg-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payg-handler.d.ts","sourceRoot":"","sources":["../../src/billing/payg-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AAW1F,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD,2EAA2E;IAC3E,oBAAoB,EAAE,CAAC,KAAK,EAAE,wBAAwB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,uBAAuB,GAAG,IAAI,GAC5C,WAAW,CAmGb"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * PAYG (Pay as You Go) command handler for the `request_payment` heartbeat command.
3
+ *
4
+ * This is deterministic bot code u2014 the LLM agent is never involved in
5
+ * payment flows. The handler:
6
+ *
7
+ * 1. Receives `request_payment` from the backend via heartbeat (includes amounts)
8
+ * 2. Sends an amount selection prompt (inline keyboard) to the owner via Telegram
9
+ * 3. Listens for `billing_payg_selected` events from the Telegram adapter
10
+ * 4. Calls the backend to create a Stripe Payment Link (one-time)
11
+ * 5. Sends the payment URL back to the owner as a URL button
12
+ */
13
+ import { logger } from "../logger.js";
14
+ const MAX_RETRIES = 2;
15
+ const RETRY_DELAY_MS = 2_000;
16
+ function sleep(ms) {
17
+ return new Promise((resolve) => setTimeout(resolve, ms));
18
+ }
19
+ /**
20
+ * Create a CommandHandler for the "request_payment" heartbeat command.
21
+ *
22
+ * @param channelManager - To send PAYG prompts and listen for amount selection events
23
+ * @param ownerChatId - The owner's Telegram chat ID
24
+ * @param serviceClient - Onboarding service client for creating payment links
25
+ */
26
+ export function createPaygHandler(channelManager, ownerChatId, serviceClient) {
27
+ async function handleAmountSelected(event) {
28
+ if (!serviceClient) {
29
+ logger.warn("[payg] No service client u2014 cannot create payment link");
30
+ await channelManager.sendMessage("telegram", String(event.chatId), "Payment is not configured. Please contact support.");
31
+ return;
32
+ }
33
+ logger.system(`[payg] Amount selected: $${event.amount} by chat ${event.chatId}`);
34
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
35
+ try {
36
+ const result = await serviceClient.createPaymentLink(event.amount, event.chatId);
37
+ if (result?.checkoutUrl) {
38
+ await channelManager.sendTelegramCheckoutLink(event.chatId, `$${event.amount} Top-Up`, result.checkoutUrl);
39
+ logger.system(`[payg] Payment link sent to chat ${event.chatId} for $${event.amount}`);
40
+ return;
41
+ }
42
+ if (attempt < MAX_RETRIES) {
43
+ logger.warn(`[payg] Payment link attempt ${attempt + 1} failed, retrying...`);
44
+ await channelManager.sendMessage("telegram", String(event.chatId), "Payment setup failed, retrying...");
45
+ await sleep(RETRY_DELAY_MS);
46
+ }
47
+ }
48
+ catch (err) {
49
+ const errMsg = err instanceof Error ? err.message : String(err);
50
+ logger.err(`[payg] Payment link attempt ${attempt + 1} error: ${errMsg}`);
51
+ if (attempt < MAX_RETRIES) {
52
+ await sleep(RETRY_DELAY_MS);
53
+ }
54
+ }
55
+ }
56
+ logger.err("[payg] All payment link attempts failed");
57
+ await channelManager.sendMessage("telegram", String(event.chatId), "Payment setup failed. Please try again later.");
58
+ }
59
+ const handler = Object.assign(async (_commandId, payload) => {
60
+ const raw = (payload ?? {});
61
+ const amounts = Array.isArray(raw.amounts) ? raw.amounts : [];
62
+ if (amounts.length === 0) {
63
+ logger.warn("[payg] request_payment received with no amounts");
64
+ return {
65
+ status: "failed",
66
+ result: JSON.stringify({ error: "No amounts in payload" }),
67
+ };
68
+ }
69
+ const typedPayload = {
70
+ reason: typeof raw.reason === "string" ? raw.reason : undefined,
71
+ amounts,
72
+ };
73
+ logger.system(`[payg] Request payment received with amounts: ${typedPayload.amounts.join(", ")}`);
74
+ try {
75
+ await channelManager.sendTelegramPaygPrompt(ownerChatId, typedPayload);
76
+ return {
77
+ status: "completed",
78
+ result: JSON.stringify({ message: "PAYG prompt sent to owner" }),
79
+ };
80
+ }
81
+ catch (err) {
82
+ const errMsg = err instanceof Error ? err.message : String(err);
83
+ logger.err(`[payg] Failed to send PAYG prompt: ${errMsg}`);
84
+ return {
85
+ status: "failed",
86
+ result: JSON.stringify({ error: errMsg }),
87
+ };
88
+ }
89
+ }, { handleAmountSelected });
90
+ return handler;
91
+ }
92
+ //# sourceMappingURL=payg-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payg-handler.js","sourceRoot":"","sources":["../../src/billing/payg-handler.ts"],"names":[],"mappings":"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"}
@@ -20319,7 +20319,7 @@ var require_prompts3 = __commonJS({
20319
20319
 
20320
20320
  // dist/utils/version-check.js
20321
20321
  function isBundled() {
20322
- const v10 = "0.1.182-beta.5";
20322
+ const v10 = "0.1.182-beta.6";
20323
20323
  return typeof v10 === "string" && v10 !== "undefined";
20324
20324
  }
20325
20325
  function getPackageRoot() {
@@ -20336,7 +20336,7 @@ function getInstallationInfo() {
20336
20336
  };
20337
20337
  }
20338
20338
  function getCurrentVersion() {
20339
- const bundledVersion = "0.1.182-beta.5";
20339
+ const bundledVersion = "0.1.182-beta.6";
20340
20340
  if (bundledVersion && bundledVersion !== "undefined") {
20341
20341
  return bundledVersion;
20342
20342
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visionclaw",
3
- "version": "0.1.182-beta.5",
3
+ "version": "0.1.182-beta.6",
4
4
  "description": "A personal assistant agent that runs on your desktop, receives commands from messaging channels, and executes tasks autonomously.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,6 +16,7 @@
16
16
  "assets/**/*",
17
17
  "packages/openclaw-sdk-shim/**/*",
18
18
  "scripts/setup-openclaw-shim.mjs",
19
+ "scripts/wechat-monitor.mjs",
19
20
  "README.md",
20
21
  "CHANGELOG.md"
21
22
  ],
@@ -115,6 +116,8 @@
115
116
  "build:bundle": "node scripts/esbuild-bundle.cjs",
116
117
  "build:obfuscate": "javascript-obfuscator dist/ --output dist/ --compact true --identifier-names-generator hexadecimal --string-array true --string-array-encoding base64 --rename-properties false",
117
118
  "release": "pnpm lint && pnpm build && pnpm version patch --no-git-tag-version && pnpm run build:bundle && pnpm changelog && git add -A && git commit -m \"chore(release): v$(node -p 'require(\"./package.json\").version')\" && git tag \"v$(node -p 'require(\"./package.json\").version')\" && pnpm publish",
119
+ "release:manual": "test -z \"$(git status --porcelain)\" && pnpm lint && pnpm build && pnpm version patch --no-git-tag-version && pnpm run build:bundle && pnpm changelog && printf '\\nRelease prepared. Edit CHANGELOG.md, then run: pnpm release:manual:finalize\\n'",
120
+ "release:manual:finalize": "git add package.json CHANGELOG.md dist dist-agent && git commit -m \"chore(release): v$(node -p 'require(\"./package.json\").version')\" && git tag \"v$(node -p 'require(\"./package.json\").version')\" && pnpm publish",
118
121
  "release:beta": "pnpm lint && pnpm build && pnpm version prerelease --preid beta --no-git-tag-version && pnpm run build:bundle && git add -A && git commit -m \"chore(release): v$(node -p 'require(\"./package.json\").version')\" && git tag \"v$(node -p 'require(\"./package.json\").version')\" && pnpm publish --tag beta"
119
122
  }
120
123
  }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * WeChat Real-Time Monitor
3
+ *
4
+ * Watches the WeChat SQLite message database for file changes.
5
+ * When a change is detected, injects a wake message into VisionClaw's queue
6
+ * and sends SIGUSR2 to the VisionClaw process for an immediate wake-up.
7
+ *
8
+ * The WeChat account directory (wxid_*) is auto-detected. If multiple accounts
9
+ * are present, the most recently modified database is used. Override by setting
10
+ * the WECHAT_WXID environment variable.
11
+ *
12
+ * Usage:
13
+ * node scripts/wechat-monitor.mjs
14
+ *
15
+ * Run under pm2 (recommended):
16
+ * pm2 start scripts/wechat-monitor.mjs --name wechat-monitor
17
+ * pm2 save
18
+ *
19
+ * Environment variables:
20
+ * WECHAT_WXID — override WeChat account ID (e.g. wxid_abc123)
21
+ * VISIONCLAW_PROFILE — VisionClaw profile name (default: "default")
22
+ */
23
+
24
+ import { watch, watchFile } from "node:fs";
25
+ import { readdirSync, statSync } from "node:fs";
26
+ import { DatabaseSync } from "node:sqlite";
27
+ import { randomUUID } from "node:crypto";
28
+ import { execSync } from "node:child_process";
29
+ import path from "node:path";
30
+ import os from "node:os";
31
+
32
+ const HOME = os.homedir();
33
+
34
+ // VisionClaw queue database — support non-default profiles via env var
35
+ const PROFILE = process.env.VISIONCLAW_PROFILE || "default";
36
+ const QUEUE_DB = path.join(HOME, `.visionclaw/profiles/${PROFILE}/queue.db`);
37
+
38
+ // Debounce: avoid flooding on rapid writes
39
+ let debounceTimer = null;
40
+ const DEBOUNCE_MS = 500;
41
+
42
+ // Cache the VisionClaw PID to avoid spawning pm2 on every trigger.
43
+ // Refreshed only when the cached PID is no longer valid.
44
+ let cachedPid = null;
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // WeChat DB path auto-detection
48
+ // ---------------------------------------------------------------------------
49
+
50
+ const XWECHAT_ROOT = path.join(
51
+ HOME,
52
+ "Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files"
53
+ );
54
+
55
+ function findWeChatMessageDb() {
56
+ // Allow explicit override via env var
57
+ const wxidOverride = process.env.WECHAT_WXID;
58
+ if (wxidOverride) {
59
+ const dbPath = path.join(XWECHAT_ROOT, wxidOverride, "db_storage/message/message_0.db");
60
+ try {
61
+ statSync(dbPath);
62
+ return dbPath;
63
+ } catch {
64
+ console.error(`[wechat-monitor] WECHAT_WXID set to '${wxidOverride}' but db not found at: ${dbPath}`);
65
+ return null;
66
+ }
67
+ }
68
+
69
+ // Auto-detect: list all wxid_* directories, pick one with a valid message_0.db
70
+ const candidates = [];
71
+ try {
72
+ const entries = readdirSync(XWECHAT_ROOT, { withFileTypes: true });
73
+ for (const entry of entries) {
74
+ if (!entry.isDirectory() || !entry.name.startsWith("wxid_")) continue;
75
+ const dbPath = path.join(XWECHAT_ROOT, entry.name, "db_storage/message/message_0.db");
76
+ try {
77
+ const st = statSync(dbPath);
78
+ candidates.push({ dbPath, mtimeMs: st.mtimeMs });
79
+ } catch {
80
+ // directory exists but has no message_0.db — skip
81
+ }
82
+ }
83
+ } catch {
84
+ console.error(`[wechat-monitor] Cannot read WeChat data directory: ${XWECHAT_ROOT}`);
85
+ console.error("[wechat-monitor] Make sure WeChat is installed and has been run at least once.");
86
+ return null;
87
+ }
88
+
89
+ if (candidates.length === 0) {
90
+ console.error("[wechat-monitor] No WeChat account databases found under:");
91
+ console.error(` ${XWECHAT_ROOT}`);
92
+ console.error("[wechat-monitor] Tip: set WECHAT_WXID=<your-wxid> to specify explicitly.");
93
+ return null;
94
+ }
95
+
96
+ if (candidates.length > 1) {
97
+ // Pick the most recently active account
98
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
99
+ console.log(
100
+ `[wechat-monitor] Multiple WeChat accounts found, using most recent: ${path.dirname(path.dirname(candidates[0].dbPath)).split("/").pop()}`
101
+ );
102
+ }
103
+
104
+ return candidates[0].dbPath;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Core logic
109
+ // ---------------------------------------------------------------------------
110
+
111
+ function getVisionClawPid() {
112
+ // Return cached PID if it's still alive
113
+ if (cachedPid !== null) {
114
+ try {
115
+ process.kill(cachedPid, 0); // signal 0 = check if process exists
116
+ return cachedPid;
117
+ } catch {
118
+ cachedPid = null; // process is gone, refresh
119
+ }
120
+ }
121
+
122
+ // Try pm2 first
123
+ try {
124
+ const out = execSync("pm2 pid visionclaw", { encoding: "utf-8" }).trim();
125
+ const pid = parseInt(out, 10);
126
+ if (!isNaN(pid) && pid > 0) {
127
+ cachedPid = pid;
128
+ return pid;
129
+ }
130
+ } catch {
131
+ // pm2 not available or process not registered
132
+ }
133
+
134
+ // Fallback: find the Electron-hosted agent process (bundle.cjs --profile)
135
+ try {
136
+ const out = execSync(
137
+ "pgrep -f 'bundle\\.cjs.*--profile'",
138
+ { encoding: "utf-8" }
139
+ ).trim();
140
+ const pid = parseInt(out.split("\n")[0], 10);
141
+ if (!isNaN(pid) && pid > 0) {
142
+ cachedPid = pid;
143
+ return pid;
144
+ }
145
+ } catch {
146
+ // No matching process found
147
+ }
148
+
149
+ return null;
150
+ }
151
+
152
+ function injectWakeMessage() {
153
+ let db;
154
+ try {
155
+ db = new DatabaseSync(QUEUE_DB);
156
+
157
+ // Deduplicate: skip if there is already a pending WeChat Monitor message in the queue
158
+ const pending = db.prepare(
159
+ `SELECT COUNT(*) as count FROM messages
160
+ WHERE sender = 'WeChat Monitor'
161
+ AND json_extract(meta, '$.kind') = 'external-wake'`
162
+ ).get();
163
+ if (pending.count > 0) {
164
+ console.log(`[wechat-monitor] Skipped — ${pending.count} pending monitor message(s) already in queue`);
165
+ return;
166
+ }
167
+
168
+ const stmt = db.prepare(
169
+ `INSERT OR IGNORE INTO messages (id, channel, sender, text, attachments, timestamp, meta)
170
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
171
+ );
172
+ stmt.run(
173
+ randomUUID(),
174
+ "wechat-monitor",
175
+ "WeChat Monitor",
176
+ "[WeChat] New message detected \u2014 please check WeChat for unread messages.",
177
+ "[]",
178
+ new Date().toISOString(),
179
+ JSON.stringify({ source: "wechat-monitor", kind: "external-wake" })
180
+ );
181
+ console.log(`[wechat-monitor] Injected wake message at ${new Date().toISOString()}`);
182
+ } catch (err) {
183
+ console.error(`[wechat-monitor] Failed to inject message: ${err.message}`);
184
+ } finally {
185
+ if (db) {
186
+ try { db.close(); } catch { /* best-effort close */ }
187
+ }
188
+ }
189
+ }
190
+
191
+ function sendSigusr2() {
192
+ const pid = getVisionClawPid();
193
+ if (!pid) {
194
+ console.warn("[wechat-monitor] VisionClaw PID not found (checked pm2 + pgrep) — skipping SIGUSR2");
195
+ return;
196
+ }
197
+ try {
198
+ process.kill(pid, "SIGUSR2");
199
+ console.log(`[wechat-monitor] Sent SIGUSR2 to VisionClaw (pid ${pid})`);
200
+ } catch (err) {
201
+ // PID may have just died — invalidate cache
202
+ cachedPid = null;
203
+ console.error(`[wechat-monitor] Failed to send SIGUSR2: ${err.message}`);
204
+ }
205
+ }
206
+
207
+ function onDatabaseChanged() {
208
+ if (debounceTimer) clearTimeout(debounceTimer);
209
+ debounceTimer = setTimeout(() => {
210
+ debounceTimer = null;
211
+ console.log(`[wechat-monitor] message_0.db changed at ${new Date().toISOString()}`);
212
+ injectWakeMessage();
213
+ sendSigusr2();
214
+ }, DEBOUNCE_MS);
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Start
219
+ // ---------------------------------------------------------------------------
220
+
221
+ const WECHAT_MESSAGE_DB = findWeChatMessageDb();
222
+ if (!WECHAT_MESSAGE_DB) {
223
+ process.exit(1);
224
+ }
225
+
226
+ // Watch the main db and the SQLite WAL files (SQLite writes to -wal first).
227
+ // The -last.material file is a WeChat-specific sidecar that tracks the latest
228
+ // materialized message offset; changes to it also signal new message activity.
229
+ const filesToWatch = [
230
+ WECHAT_MESSAGE_DB,
231
+ WECHAT_MESSAGE_DB + "-wal",
232
+ WECHAT_MESSAGE_DB + "-last.material",
233
+ ];
234
+
235
+ let eventWatchCount = 0;
236
+ let pollWatchCount = 0;
237
+
238
+ for (const file of filesToWatch) {
239
+ // Use fs.watch for immediate event-driven notifications
240
+ try {
241
+ watch(file, { persistent: true }, (eventType) => {
242
+ if (eventType === "change") onDatabaseChanged();
243
+ });
244
+ eventWatchCount++;
245
+ console.log(`[wechat-monitor] Watching (event): ${path.basename(file)}`);
246
+ } catch (err) {
247
+ console.warn(`[wechat-monitor] Could not watch ${path.basename(file)}: ${err.message}`);
248
+ }
249
+ }
250
+
251
+ // Additionally use fs.watchFile (stat-based polling) on the main db and WAL as
252
+ // a fallback. fs.watch can miss changes when SQLite uses mmap or fcntl locking
253
+ // without updating mtime. Polling every 2 seconds is a reliable safety net.
254
+ const POLL_INTERVAL_MS = 2000;
255
+ const pollFiles = [
256
+ WECHAT_MESSAGE_DB,
257
+ WECHAT_MESSAGE_DB + "-wal",
258
+ ];
259
+ for (const file of pollFiles) {
260
+ try {
261
+ watchFile(file, { persistent: true, interval: POLL_INTERVAL_MS }, (curr, prev) => {
262
+ if (curr.mtimeMs !== prev.mtimeMs || curr.size !== prev.size) {
263
+ onDatabaseChanged();
264
+ }
265
+ });
266
+ pollWatchCount++;
267
+ console.log(`[wechat-monitor] Watching (poll ${POLL_INTERVAL_MS}ms): ${path.basename(file)}`);
268
+ } catch (err) {
269
+ console.warn(`[wechat-monitor] Could not poll-watch ${path.basename(file)}: ${err.message}`);
270
+ }
271
+ }
272
+
273
+ const totalWatchCount = eventWatchCount + pollWatchCount;
274
+ if (totalWatchCount === 0) {
275
+ console.error("[wechat-monitor] No files could be watched. Exiting.");
276
+ process.exit(1);
277
+ }
278
+
279
+ console.log(`[wechat-monitor] Started. Watching ${eventWatchCount} event + ${pollWatchCount} poll watcher(s) for WeChat activity.`);
280
+ console.log(`[wechat-monitor] Profile: ${PROFILE}`);
281
+ console.log(`[wechat-monitor] Queue DB: ${QUEUE_DB}`);
282
+ console.log(`[wechat-monitor] WeChat DB: ${WECHAT_MESSAGE_DB}`);