@wahooks/channel 2.0.0 → 2.1.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/dist/daemon.d.ts +11 -0
- package/dist/daemon.js +120 -0
- package/package.json +3 -2
package/dist/daemon.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WAHooks Reminder Daemon
|
|
4
|
+
*
|
|
5
|
+
* Runs in the background (via launchd). Every 30s, checks
|
|
6
|
+
* reminders.json for due items and appends them to pending.json.
|
|
7
|
+
* The channel picks up pending items and delivers them to Claude.
|
|
8
|
+
*
|
|
9
|
+
* Packaged with @wahooks/channel — binary: wahooks-reminders
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WAHooks Reminder Daemon
|
|
4
|
+
*
|
|
5
|
+
* Runs in the background (via launchd). Every 30s, checks
|
|
6
|
+
* reminders.json for due items and appends them to pending.json.
|
|
7
|
+
* The channel picks up pending items and delivers them to Claude.
|
|
8
|
+
*
|
|
9
|
+
* Packaged with @wahooks/channel — binary: wahooks-reminders
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
const WAHOOKS_DIR = path.join(os.homedir(), ".wahooks");
|
|
15
|
+
const REMINDERS_FILE = path.join(WAHOOKS_DIR, "reminders.json");
|
|
16
|
+
const PENDING_FILE = path.join(WAHOOKS_DIR, "pending.json");
|
|
17
|
+
const LOCK_FILE = path.join(WAHOOKS_DIR, ".pending.lock");
|
|
18
|
+
const CHECK_INTERVAL = 30_000;
|
|
19
|
+
const STALE_HOURS = 24;
|
|
20
|
+
function acquireLock() {
|
|
21
|
+
try {
|
|
22
|
+
fs.writeFileSync(LOCK_FILE, String(process.pid), { flag: "wx" });
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
try {
|
|
27
|
+
const pid = parseInt(fs.readFileSync(LOCK_FILE, "utf-8"));
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
fs.writeFileSync(LOCK_FILE, String(process.pid));
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function releaseLock() {
|
|
43
|
+
try {
|
|
44
|
+
fs.unlinkSync(LOCK_FILE);
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
}
|
|
48
|
+
function readJson(file, fallback) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function writeJson(file, data) {
|
|
57
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
58
|
+
}
|
|
59
|
+
function checkReminders(parseExpression) {
|
|
60
|
+
if (!acquireLock())
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
const reminders = readJson(REMINDERS_FILE, {});
|
|
64
|
+
const pending = readJson(PENDING_FILE, []);
|
|
65
|
+
const now = new Date();
|
|
66
|
+
let changed = false;
|
|
67
|
+
let pendingChanged = false;
|
|
68
|
+
// Prune stale (>24h)
|
|
69
|
+
const cutoff = new Date(now.getTime() - STALE_HOURS * 60 * 60 * 1000);
|
|
70
|
+
const active = pending.filter((p) => new Date(p.addedAt) > cutoff);
|
|
71
|
+
if (active.length !== pending.length)
|
|
72
|
+
pendingChanged = true;
|
|
73
|
+
for (const [id, rem] of Object.entries(reminders)) {
|
|
74
|
+
if (new Date(rem.nextRunAt) <= now) {
|
|
75
|
+
active.push({
|
|
76
|
+
reminderId: id,
|
|
77
|
+
task: rem.task,
|
|
78
|
+
chatId: rem.chatId,
|
|
79
|
+
scheduledFor: rem.nextRunAt,
|
|
80
|
+
addedAt: now.toISOString(),
|
|
81
|
+
});
|
|
82
|
+
pendingChanged = true;
|
|
83
|
+
if (rem.oneTime) {
|
|
84
|
+
delete reminders[id];
|
|
85
|
+
changed = true;
|
|
86
|
+
console.log(`[reminders] Fired one-time: ${id}`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
try {
|
|
90
|
+
const next = parseExpression(rem.schedule).next().toDate();
|
|
91
|
+
reminders[id] = { ...rem, lastFiredAt: now.toISOString(), nextRunAt: next.toISOString() };
|
|
92
|
+
changed = true;
|
|
93
|
+
console.log(`[reminders] Fired recurring: ${id}, next: ${next.toISOString()}`);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
console.error(`[reminders] Invalid cron for ${id}: ${rem.schedule}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (changed)
|
|
102
|
+
writeJson(REMINDERS_FILE, reminders);
|
|
103
|
+
if (pendingChanged)
|
|
104
|
+
writeJson(PENDING_FILE, active);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
releaseLock();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function main() {
|
|
111
|
+
fs.mkdirSync(WAHOOKS_DIR, { recursive: true });
|
|
112
|
+
const { parseExpression } = await import("cron-parser");
|
|
113
|
+
console.log(`[reminders] Daemon started — checking every ${CHECK_INTERVAL / 1000}s`);
|
|
114
|
+
checkReminders(parseExpression);
|
|
115
|
+
setInterval(() => checkReminders(parseExpression), CHECK_INTERVAL);
|
|
116
|
+
}
|
|
117
|
+
main().catch((err) => {
|
|
118
|
+
console.error("[reminders] Fatal:", err.message);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wahooks/channel",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "WhatsApp channel for Claude Code — chat with Claude via WhatsApp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"wahooks-channel": "dist/index.js"
|
|
7
|
+
"wahooks-channel": "dist/index.js",
|
|
8
|
+
"wahooks-reminders": "dist/daemon.js"
|
|
8
9
|
},
|
|
9
10
|
"main": "dist/index.js",
|
|
10
11
|
"scripts": {
|