codeksei 0.1.0 → 0.1.1

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/LICENSE +661 -661
  2. package/README.en.md +109 -47
  3. package/README.md +79 -58
  4. package/bin/cyberboss.js +1 -1
  5. package/package.json +86 -86
  6. package/scripts/open_shared_wechat_thread.sh +77 -77
  7. package/scripts/open_wechat_thread.sh +108 -108
  8. package/scripts/shared-common.js +144 -144
  9. package/scripts/shared-open.js +14 -14
  10. package/scripts/shared-start.js +5 -5
  11. package/scripts/shared-status.js +27 -27
  12. package/scripts/show_shared_status.sh +45 -45
  13. package/scripts/start_shared_app_server.sh +52 -52
  14. package/scripts/start_shared_wechat.sh +94 -94
  15. package/scripts/timeline-screenshot.sh +14 -14
  16. package/src/adapters/channel/weixin/account-store.js +99 -99
  17. package/src/adapters/channel/weixin/api-v2.js +50 -50
  18. package/src/adapters/channel/weixin/api.js +169 -169
  19. package/src/adapters/channel/weixin/context-token-store.js +84 -84
  20. package/src/adapters/channel/weixin/index.js +618 -604
  21. package/src/adapters/channel/weixin/legacy.js +579 -566
  22. package/src/adapters/channel/weixin/media-mime.js +22 -22
  23. package/src/adapters/channel/weixin/media-receive.js +370 -370
  24. package/src/adapters/channel/weixin/media-send.js +102 -102
  25. package/src/adapters/channel/weixin/message-utils-v2.js +282 -282
  26. package/src/adapters/channel/weixin/message-utils.js +199 -199
  27. package/src/adapters/channel/weixin/redact.js +41 -41
  28. package/src/adapters/channel/weixin/reminder-queue-store.js +101 -101
  29. package/src/adapters/channel/weixin/sync-buffer-store.js +35 -35
  30. package/src/adapters/runtime/codex/events.js +215 -215
  31. package/src/adapters/runtime/codex/index.js +109 -104
  32. package/src/adapters/runtime/codex/message-utils.js +95 -95
  33. package/src/adapters/runtime/codex/model-catalog.js +106 -106
  34. package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -75
  35. package/src/adapters/runtime/codex/rpc-client.js +339 -339
  36. package/src/adapters/runtime/codex/session-store.js +286 -286
  37. package/src/app/channel-send-file-cli.js +57 -57
  38. package/src/app/diary-write-cli.js +236 -88
  39. package/src/app/note-sync-cli.js +2 -2
  40. package/src/app/reminder-write-cli.js +215 -210
  41. package/src/app/review-cli.js +7 -5
  42. package/src/app/system-checkin-poller.js +64 -64
  43. package/src/app/system-send-cli.js +129 -129
  44. package/src/app/timeline-event-cli.js +28 -25
  45. package/src/app/timeline-screenshot-cli.js +103 -100
  46. package/src/core/app.js +1763 -1763
  47. package/src/core/branding.js +2 -1
  48. package/src/core/command-registry.js +381 -369
  49. package/src/core/config.js +30 -14
  50. package/src/core/default-targets.js +163 -163
  51. package/src/core/durable-note-schema.js +9 -8
  52. package/src/core/instructions-template.js +17 -16
  53. package/src/core/note-sync.js +8 -7
  54. package/src/core/path-utils.js +54 -0
  55. package/src/core/project-radar.js +11 -10
  56. package/src/core/review.js +48 -50
  57. package/src/core/stream-delivery.js +1162 -983
  58. package/src/core/system-message-dispatcher.js +68 -68
  59. package/src/core/system-message-queue-store.js +128 -128
  60. package/src/core/thread-state-store.js +96 -96
  61. package/src/core/timeline-screenshot-queue-store.js +134 -134
  62. package/src/core/timezone.js +436 -0
  63. package/src/core/workspace-bootstrap.js +9 -1
  64. package/src/index.js +148 -146
  65. package/src/integrations/timeline/index.js +130 -74
  66. package/src/integrations/timeline/state-sync.js +240 -0
  67. package/templates/weixin-instructions.md +12 -38
  68. package/templates/weixin-operations.md +29 -31
@@ -1,199 +1,199 @@
1
- const TEXT_ITEM_TYPE = 1;
2
- const IMAGE_ITEM_TYPE = 2;
3
- const VOICE_ITEM_TYPE = 3;
4
- const FILE_ITEM_TYPE = 4;
5
- const VIDEO_ITEM_TYPE = 5;
6
- const BOT_MESSAGE_TYPE = 2;
7
-
8
- function normalizeWeixinIncomingMessage(message, config, accountId) {
9
- if (!message || typeof message !== "object") {
10
- return null;
11
- }
12
- if (Number(message.message_type) === BOT_MESSAGE_TYPE) {
13
- return null;
14
- }
15
-
16
- const senderId = normalizeText(message.from_user_id);
17
- if (!senderId) {
18
- return null;
19
- }
20
-
21
- const text = extractTextBody(message.item_list);
22
- const attachments = extractAttachmentItems(message.item_list);
23
- if (!text && !attachments.length) {
24
- return null;
25
- }
26
-
27
- return {
28
- provider: "weixin",
29
- accountId,
30
- workspaceId: config.workspaceId,
31
- senderId,
32
- chatId: senderId,
33
- messageId: normalizeText(message.message_id),
34
- threadKey: normalizeText(message.session_id),
35
- text,
36
- attachments,
37
- contextToken: normalizeText(message.context_token),
38
- receivedAt: resolveReceivedAt(message),
39
- };
40
- }
41
-
42
- function extractTextBody(itemList) {
43
- if (!Array.isArray(itemList) || !itemList.length) {
44
- return "";
45
- }
46
-
47
- for (const item of itemList) {
48
- if (Number(item?.type) === TEXT_ITEM_TYPE && typeof item?.text_item?.text === "string") {
49
- return item.text_item.text.trim();
50
- }
51
- if (Number(item?.type) === VOICE_ITEM_TYPE && typeof item?.voice_item?.text === "string") {
52
- return item.voice_item.text.trim();
53
- }
54
- }
55
-
56
- return "";
57
- }
58
-
59
- function extractAttachmentItems(itemList) {
60
- if (!Array.isArray(itemList) || !itemList.length) {
61
- return [];
62
- }
63
-
64
- const attachments = [];
65
- for (let index = 0; index < itemList.length; index += 1) {
66
- const normalized = normalizeAttachmentItem(itemList[index], index);
67
- if (normalized) {
68
- attachments.push(normalized);
69
- }
70
- }
71
-
72
- return attachments;
73
- }
74
-
75
- function normalizeAttachmentItem(item, index) {
76
- const itemType = Number(item?.type);
77
- const payload = resolveAttachmentPayload(itemType, item);
78
- if (!payload) {
79
- return null;
80
- }
81
-
82
- const media = payload.media && typeof payload.media === "object"
83
- ? payload.media
84
- : {};
85
-
86
- return {
87
- kind: payload.kind,
88
- itemType,
89
- index,
90
- fileName: normalizeText(
91
- payload.body?.file_name
92
- || payload.body?.filename
93
- || item?.file_name
94
- || item?.filename
95
- ),
96
- sizeBytes: parseOptionalInt(
97
- payload.body?.len
98
- || payload.body?.file_size
99
- || payload.body?.size
100
- || payload.body?.video_size
101
- || item?.len
102
- ),
103
- directUrls: collectStringValues([
104
- payload.body?.url,
105
- payload.body?.download_url,
106
- payload.body?.cdn_url,
107
- media?.url,
108
- media?.download_url,
109
- media?.cdn_url,
110
- ]),
111
- mediaRef: {
112
- encryptQueryParam: normalizeText(
113
- media?.encrypt_query_param
114
- || media?.encrypted_query_param
115
- || payload.body?.encrypt_query_param
116
- || payload.body?.encrypted_query_param
117
- || item?.encrypt_query_param
118
- || item?.encrypted_query_param
119
- ),
120
- aesKey: normalizeText(
121
- media?.aes_key
122
- || payload.body?.aes_key
123
- || item?.aes_key
124
- ),
125
- aesKeyHex: normalizeText(
126
- payload.body?.aeskey
127
- || payload.body?.aes_key_hex
128
- || item?.aeskey
129
- ),
130
- encryptType: Number(
131
- media?.encrypt_type
132
- ?? payload.body?.encrypt_type
133
- ?? item?.encrypt_type
134
- ?? 1
135
- ),
136
- fileKey: normalizeText(
137
- media?.filekey
138
- || payload.body?.filekey
139
- || item?.filekey
140
- ),
141
- },
142
- rawItem: item,
143
- };
144
- }
145
-
146
- function resolveAttachmentPayload(itemType, item) {
147
- if (itemType === IMAGE_ITEM_TYPE && item?.image_item && typeof item.image_item === "object") {
148
- return { kind: "image", body: item.image_item, media: item.image_item.media };
149
- }
150
- if (itemType === FILE_ITEM_TYPE && item?.file_item && typeof item.file_item === "object") {
151
- return { kind: "file", body: item.file_item, media: item.file_item.media };
152
- }
153
- if (itemType === VIDEO_ITEM_TYPE && item?.video_item && typeof item.video_item === "object") {
154
- return { kind: "video", body: item.video_item, media: item.video_item.media };
155
- }
156
- return null;
157
- }
158
-
159
- function collectStringValues(values) {
160
- const seen = new Set();
161
- const result = [];
162
- for (const value of values) {
163
- const normalized = normalizeText(value);
164
- if (!normalized || seen.has(normalized)) {
165
- continue;
166
- }
167
- seen.add(normalized);
168
- result.push(normalized);
169
- }
170
- return result;
171
- }
172
-
173
- function parseOptionalInt(value) {
174
- if (value == null || value === "") {
175
- return 0;
176
- }
177
- const parsed = Number.parseInt(String(value), 10);
178
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
179
- }
180
-
181
- function normalizeText(value) {
182
- return typeof value === "string" ? value.trim() : "";
183
- }
184
-
185
- function resolveReceivedAt(message) {
186
- const rawMs = Number(message?.create_time_ms);
187
- if (Number.isFinite(rawMs) && rawMs > 0) {
188
- return new Date(rawMs).toISOString();
189
- }
190
- const rawSeconds = Number(message?.create_time);
191
- if (Number.isFinite(rawSeconds) && rawSeconds > 0) {
192
- return new Date(rawSeconds * 1000).toISOString();
193
- }
194
- return new Date().toISOString();
195
- }
196
-
197
- module.exports = {
198
- normalizeWeixinIncomingMessage,
199
- };
1
+ const TEXT_ITEM_TYPE = 1;
2
+ const IMAGE_ITEM_TYPE = 2;
3
+ const VOICE_ITEM_TYPE = 3;
4
+ const FILE_ITEM_TYPE = 4;
5
+ const VIDEO_ITEM_TYPE = 5;
6
+ const BOT_MESSAGE_TYPE = 2;
7
+
8
+ function normalizeWeixinIncomingMessage(message, config, accountId) {
9
+ if (!message || typeof message !== "object") {
10
+ return null;
11
+ }
12
+ if (Number(message.message_type) === BOT_MESSAGE_TYPE) {
13
+ return null;
14
+ }
15
+
16
+ const senderId = normalizeText(message.from_user_id);
17
+ if (!senderId) {
18
+ return null;
19
+ }
20
+
21
+ const text = extractTextBody(message.item_list);
22
+ const attachments = extractAttachmentItems(message.item_list);
23
+ if (!text && !attachments.length) {
24
+ return null;
25
+ }
26
+
27
+ return {
28
+ provider: "weixin",
29
+ accountId,
30
+ workspaceId: config.workspaceId,
31
+ senderId,
32
+ chatId: senderId,
33
+ messageId: normalizeText(message.message_id),
34
+ threadKey: normalizeText(message.session_id),
35
+ text,
36
+ attachments,
37
+ contextToken: normalizeText(message.context_token),
38
+ receivedAt: resolveReceivedAt(message),
39
+ };
40
+ }
41
+
42
+ function extractTextBody(itemList) {
43
+ if (!Array.isArray(itemList) || !itemList.length) {
44
+ return "";
45
+ }
46
+
47
+ for (const item of itemList) {
48
+ if (Number(item?.type) === TEXT_ITEM_TYPE && typeof item?.text_item?.text === "string") {
49
+ return item.text_item.text.trim();
50
+ }
51
+ if (Number(item?.type) === VOICE_ITEM_TYPE && typeof item?.voice_item?.text === "string") {
52
+ return item.voice_item.text.trim();
53
+ }
54
+ }
55
+
56
+ return "";
57
+ }
58
+
59
+ function extractAttachmentItems(itemList) {
60
+ if (!Array.isArray(itemList) || !itemList.length) {
61
+ return [];
62
+ }
63
+
64
+ const attachments = [];
65
+ for (let index = 0; index < itemList.length; index += 1) {
66
+ const normalized = normalizeAttachmentItem(itemList[index], index);
67
+ if (normalized) {
68
+ attachments.push(normalized);
69
+ }
70
+ }
71
+
72
+ return attachments;
73
+ }
74
+
75
+ function normalizeAttachmentItem(item, index) {
76
+ const itemType = Number(item?.type);
77
+ const payload = resolveAttachmentPayload(itemType, item);
78
+ if (!payload) {
79
+ return null;
80
+ }
81
+
82
+ const media = payload.media && typeof payload.media === "object"
83
+ ? payload.media
84
+ : {};
85
+
86
+ return {
87
+ kind: payload.kind,
88
+ itemType,
89
+ index,
90
+ fileName: normalizeText(
91
+ payload.body?.file_name
92
+ || payload.body?.filename
93
+ || item?.file_name
94
+ || item?.filename
95
+ ),
96
+ sizeBytes: parseOptionalInt(
97
+ payload.body?.len
98
+ || payload.body?.file_size
99
+ || payload.body?.size
100
+ || payload.body?.video_size
101
+ || item?.len
102
+ ),
103
+ directUrls: collectStringValues([
104
+ payload.body?.url,
105
+ payload.body?.download_url,
106
+ payload.body?.cdn_url,
107
+ media?.url,
108
+ media?.download_url,
109
+ media?.cdn_url,
110
+ ]),
111
+ mediaRef: {
112
+ encryptQueryParam: normalizeText(
113
+ media?.encrypt_query_param
114
+ || media?.encrypted_query_param
115
+ || payload.body?.encrypt_query_param
116
+ || payload.body?.encrypted_query_param
117
+ || item?.encrypt_query_param
118
+ || item?.encrypted_query_param
119
+ ),
120
+ aesKey: normalizeText(
121
+ media?.aes_key
122
+ || payload.body?.aes_key
123
+ || item?.aes_key
124
+ ),
125
+ aesKeyHex: normalizeText(
126
+ payload.body?.aeskey
127
+ || payload.body?.aes_key_hex
128
+ || item?.aeskey
129
+ ),
130
+ encryptType: Number(
131
+ media?.encrypt_type
132
+ ?? payload.body?.encrypt_type
133
+ ?? item?.encrypt_type
134
+ ?? 1
135
+ ),
136
+ fileKey: normalizeText(
137
+ media?.filekey
138
+ || payload.body?.filekey
139
+ || item?.filekey
140
+ ),
141
+ },
142
+ rawItem: item,
143
+ };
144
+ }
145
+
146
+ function resolveAttachmentPayload(itemType, item) {
147
+ if (itemType === IMAGE_ITEM_TYPE && item?.image_item && typeof item.image_item === "object") {
148
+ return { kind: "image", body: item.image_item, media: item.image_item.media };
149
+ }
150
+ if (itemType === FILE_ITEM_TYPE && item?.file_item && typeof item.file_item === "object") {
151
+ return { kind: "file", body: item.file_item, media: item.file_item.media };
152
+ }
153
+ if (itemType === VIDEO_ITEM_TYPE && item?.video_item && typeof item.video_item === "object") {
154
+ return { kind: "video", body: item.video_item, media: item.video_item.media };
155
+ }
156
+ return null;
157
+ }
158
+
159
+ function collectStringValues(values) {
160
+ const seen = new Set();
161
+ const result = [];
162
+ for (const value of values) {
163
+ const normalized = normalizeText(value);
164
+ if (!normalized || seen.has(normalized)) {
165
+ continue;
166
+ }
167
+ seen.add(normalized);
168
+ result.push(normalized);
169
+ }
170
+ return result;
171
+ }
172
+
173
+ function parseOptionalInt(value) {
174
+ if (value == null || value === "") {
175
+ return 0;
176
+ }
177
+ const parsed = Number.parseInt(String(value), 10);
178
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
179
+ }
180
+
181
+ function normalizeText(value) {
182
+ return typeof value === "string" ? value.trim() : "";
183
+ }
184
+
185
+ function resolveReceivedAt(message) {
186
+ const rawMs = Number(message?.create_time_ms);
187
+ if (Number.isFinite(rawMs) && rawMs > 0) {
188
+ return new Date(rawMs).toISOString();
189
+ }
190
+ const rawSeconds = Number(message?.create_time);
191
+ if (Number.isFinite(rawSeconds) && rawSeconds > 0) {
192
+ return new Date(rawSeconds * 1000).toISOString();
193
+ }
194
+ return new Date().toISOString();
195
+ }
196
+
197
+ module.exports = {
198
+ normalizeWeixinIncomingMessage,
199
+ };
@@ -1,41 +1,41 @@
1
- const SENSITIVE_FIELD_NAMES = [
2
- "context_token",
3
- "bot_token",
4
- "token",
5
- "authorization",
6
- "Authorization",
7
- "aeskey",
8
- "aes_key",
9
- "upload_param",
10
- "encrypted_query_param",
11
- ];
12
-
13
- const JSON_FIELD_PATTERN = new RegExp(
14
- `"(${SENSITIVE_FIELD_NAMES.join("|")})"\\s*:\\s*"[^"]*"`,
15
- "g"
16
- );
17
-
18
- const QUERY_FIELD_PATTERN = new RegExp(
19
- `([?&](?:${SENSITIVE_FIELD_NAMES.join("|")})=)[^&\\s]+`,
20
- "g"
21
- );
22
-
23
- const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/g;
24
-
25
- function redactSensitiveText(input, maxLen = 800) {
26
- const text = typeof input === "string" ? input : String(input || "");
27
- if (!text) {
28
- return "";
29
- }
30
- const redacted = text
31
- .replace(JSON_FIELD_PATTERN, '"$1":"<redacted>"')
32
- .replace(QUERY_FIELD_PATTERN, "$1<redacted>")
33
- .replace(BEARER_PATTERN, "Bearer <redacted>");
34
- if (redacted.length <= maxLen) {
35
- return redacted;
36
- }
37
- return `${redacted.slice(0, maxLen)}…(truncated, totalLen=${redacted.length})`;
38
- }
39
-
40
- module.exports = { redactSensitiveText };
41
-
1
+ const SENSITIVE_FIELD_NAMES = [
2
+ "context_token",
3
+ "bot_token",
4
+ "token",
5
+ "authorization",
6
+ "Authorization",
7
+ "aeskey",
8
+ "aes_key",
9
+ "upload_param",
10
+ "encrypted_query_param",
11
+ ];
12
+
13
+ const JSON_FIELD_PATTERN = new RegExp(
14
+ `"(${SENSITIVE_FIELD_NAMES.join("|")})"\\s*:\\s*"[^"]*"`,
15
+ "g"
16
+ );
17
+
18
+ const QUERY_FIELD_PATTERN = new RegExp(
19
+ `([?&](?:${SENSITIVE_FIELD_NAMES.join("|")})=)[^&\\s]+`,
20
+ "g"
21
+ );
22
+
23
+ const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/g;
24
+
25
+ function redactSensitiveText(input, maxLen = 800) {
26
+ const text = typeof input === "string" ? input : String(input || "");
27
+ if (!text) {
28
+ return "";
29
+ }
30
+ const redacted = text
31
+ .replace(JSON_FIELD_PATTERN, '"$1":"<redacted>"')
32
+ .replace(QUERY_FIELD_PATTERN, "$1<redacted>")
33
+ .replace(BEARER_PATTERN, "Bearer <redacted>");
34
+ if (redacted.length <= maxLen) {
35
+ return redacted;
36
+ }
37
+ return `${redacted.slice(0, maxLen)}…(truncated, totalLen=${redacted.length})`;
38
+ }
39
+
40
+ module.exports = { redactSensitiveText };
41
+
@@ -1,101 +1,101 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
-
4
- class ReminderQueueStore {
5
- constructor({ filePath }) {
6
- this.filePath = filePath;
7
- this.state = { reminders: [] };
8
- this.ensureParentDirectory();
9
- this.load();
10
- }
11
-
12
- ensureParentDirectory() {
13
- fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
14
- }
15
-
16
- load() {
17
- try {
18
- const raw = fs.readFileSync(this.filePath, "utf8");
19
- const parsed = JSON.parse(raw);
20
- const reminders = Array.isArray(parsed?.reminders) ? parsed.reminders : [];
21
- this.state = {
22
- reminders: reminders
23
- .map(normalizeReminder)
24
- .filter(Boolean)
25
- .sort((left, right) => left.dueAtMs - right.dueAtMs),
26
- };
27
- } catch {
28
- this.state = { reminders: [] };
29
- }
30
- }
31
-
32
- save() {
33
- fs.writeFileSync(this.filePath, JSON.stringify(this.state, null, 2));
34
- }
35
-
36
- enqueue(reminder) {
37
- this.load();
38
- const normalized = normalizeReminder(reminder);
39
- if (!normalized) {
40
- throw new Error("invalid reminder");
41
- }
42
- this.state.reminders.push(normalized);
43
- this.state.reminders.sort((left, right) => left.dueAtMs - right.dueAtMs);
44
- this.save();
45
- return normalized;
46
- }
47
-
48
- listDue(nowMs = Date.now()) {
49
- this.load();
50
- const due = [];
51
- const pending = [];
52
-
53
- for (const reminder of this.state.reminders) {
54
- if (reminder.dueAtMs <= nowMs) {
55
- due.push(reminder);
56
- } else {
57
- pending.push(reminder);
58
- }
59
- }
60
-
61
- if (due.length) {
62
- this.state.reminders = pending;
63
- this.save();
64
- }
65
-
66
- return due;
67
- }
68
-
69
- peekNextDueAtMs() {
70
- this.load();
71
- const first = this.state.reminders[0];
72
- return Number.isFinite(first?.dueAtMs) ? first.dueAtMs : 0;
73
- }
74
- }
75
-
76
- function normalizeReminder(reminder) {
77
- if (!reminder || typeof reminder !== "object") {
78
- return null;
79
- }
80
- const id = typeof reminder.id === "string" ? reminder.id.trim() : "";
81
- const accountId = typeof reminder.accountId === "string" ? reminder.accountId.trim() : "";
82
- const senderId = typeof reminder.senderId === "string" ? reminder.senderId.trim() : "";
83
- const contextToken = typeof reminder.contextToken === "string" ? reminder.contextToken.trim() : "";
84
- const text = typeof reminder.text === "string" ? reminder.text.trim() : "";
85
- const dueAtMs = Number(reminder.dueAtMs);
86
- const createdAt = typeof reminder.createdAt === "string" ? reminder.createdAt.trim() : "";
87
- if (!id || !accountId || !senderId || !contextToken || !text || !Number.isFinite(dueAtMs) || dueAtMs <= 0) {
88
- return null;
89
- }
90
- return {
91
- id,
92
- accountId,
93
- senderId,
94
- contextToken,
95
- text,
96
- dueAtMs,
97
- createdAt: createdAt || new Date().toISOString(),
98
- };
99
- }
100
-
101
- module.exports = { ReminderQueueStore };
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ class ReminderQueueStore {
5
+ constructor({ filePath }) {
6
+ this.filePath = filePath;
7
+ this.state = { reminders: [] };
8
+ this.ensureParentDirectory();
9
+ this.load();
10
+ }
11
+
12
+ ensureParentDirectory() {
13
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
14
+ }
15
+
16
+ load() {
17
+ try {
18
+ const raw = fs.readFileSync(this.filePath, "utf8");
19
+ const parsed = JSON.parse(raw);
20
+ const reminders = Array.isArray(parsed?.reminders) ? parsed.reminders : [];
21
+ this.state = {
22
+ reminders: reminders
23
+ .map(normalizeReminder)
24
+ .filter(Boolean)
25
+ .sort((left, right) => left.dueAtMs - right.dueAtMs),
26
+ };
27
+ } catch {
28
+ this.state = { reminders: [] };
29
+ }
30
+ }
31
+
32
+ save() {
33
+ fs.writeFileSync(this.filePath, JSON.stringify(this.state, null, 2));
34
+ }
35
+
36
+ enqueue(reminder) {
37
+ this.load();
38
+ const normalized = normalizeReminder(reminder);
39
+ if (!normalized) {
40
+ throw new Error("invalid reminder");
41
+ }
42
+ this.state.reminders.push(normalized);
43
+ this.state.reminders.sort((left, right) => left.dueAtMs - right.dueAtMs);
44
+ this.save();
45
+ return normalized;
46
+ }
47
+
48
+ listDue(nowMs = Date.now()) {
49
+ this.load();
50
+ const due = [];
51
+ const pending = [];
52
+
53
+ for (const reminder of this.state.reminders) {
54
+ if (reminder.dueAtMs <= nowMs) {
55
+ due.push(reminder);
56
+ } else {
57
+ pending.push(reminder);
58
+ }
59
+ }
60
+
61
+ if (due.length) {
62
+ this.state.reminders = pending;
63
+ this.save();
64
+ }
65
+
66
+ return due;
67
+ }
68
+
69
+ peekNextDueAtMs() {
70
+ this.load();
71
+ const first = this.state.reminders[0];
72
+ return Number.isFinite(first?.dueAtMs) ? first.dueAtMs : 0;
73
+ }
74
+ }
75
+
76
+ function normalizeReminder(reminder) {
77
+ if (!reminder || typeof reminder !== "object") {
78
+ return null;
79
+ }
80
+ const id = typeof reminder.id === "string" ? reminder.id.trim() : "";
81
+ const accountId = typeof reminder.accountId === "string" ? reminder.accountId.trim() : "";
82
+ const senderId = typeof reminder.senderId === "string" ? reminder.senderId.trim() : "";
83
+ const contextToken = typeof reminder.contextToken === "string" ? reminder.contextToken.trim() : "";
84
+ const text = typeof reminder.text === "string" ? reminder.text.trim() : "";
85
+ const dueAtMs = Number(reminder.dueAtMs);
86
+ const createdAt = typeof reminder.createdAt === "string" ? reminder.createdAt.trim() : "";
87
+ if (!id || !accountId || !senderId || !contextToken || !text || !Number.isFinite(dueAtMs) || dueAtMs <= 0) {
88
+ return null;
89
+ }
90
+ return {
91
+ id,
92
+ accountId,
93
+ senderId,
94
+ contextToken,
95
+ text,
96
+ dueAtMs,
97
+ createdAt: createdAt || new Date().toISOString(),
98
+ };
99
+ }
100
+
101
+ module.exports = { ReminderQueueStore };