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,134 +1,134 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
-
4
- class TimelineScreenshotQueueStore {
5
- constructor({ filePath }) {
6
- this.filePath = filePath;
7
- this.state = { jobs: [] };
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 jobs = Array.isArray(parsed?.jobs) ? parsed.jobs : [];
21
- this.state = {
22
- jobs: jobs
23
- .map(normalizeTimelineScreenshotJob)
24
- .filter(Boolean)
25
- .sort(compareTimelineScreenshotJobs),
26
- };
27
- } catch {
28
- this.state = { jobs: [] };
29
- }
30
- }
31
-
32
- save() {
33
- fs.writeFileSync(this.filePath, JSON.stringify(this.state, null, 2));
34
- }
35
-
36
- enqueue(job) {
37
- this.load();
38
- const normalized = normalizeTimelineScreenshotJob(job);
39
- if (!normalized) {
40
- throw new Error("invalid timeline screenshot job");
41
- }
42
- this.state.jobs.push(normalized);
43
- this.state.jobs.sort(compareTimelineScreenshotJobs);
44
- this.save();
45
- return normalized;
46
- }
47
-
48
- drainForAccount(accountId) {
49
- this.load();
50
- const normalizedAccountId = normalizeText(accountId);
51
- const drained = [];
52
- const pending = [];
53
-
54
- for (const job of this.state.jobs) {
55
- if (job.accountId === normalizedAccountId) {
56
- drained.push(job);
57
- } else {
58
- pending.push(job);
59
- }
60
- }
61
-
62
- if (drained.length) {
63
- this.state.jobs = pending;
64
- this.save();
65
- }
66
-
67
- return drained;
68
- }
69
-
70
- hasPendingForAccount(accountId) {
71
- this.load();
72
- const normalizedAccountId = normalizeText(accountId);
73
- return this.state.jobs.some((job) => job.accountId === normalizedAccountId);
74
- }
75
- }
76
-
77
- function normalizeTimelineScreenshotJob(job) {
78
- if (!job || typeof job !== "object") {
79
- return null;
80
- }
81
-
82
- const id = normalizeText(job.id);
83
- const accountId = normalizeText(job.accountId);
84
- const senderId = normalizeText(job.senderId);
85
- const outputFile = normalizeText(job.outputFile);
86
- const createdAt = normalizeIsoTime(job.createdAt);
87
- const args = normalizeArgs(job.args);
88
-
89
- if (!id || !accountId || !senderId) {
90
- return null;
91
- }
92
-
93
- return {
94
- id,
95
- accountId,
96
- senderId,
97
- outputFile,
98
- args,
99
- createdAt: createdAt || new Date().toISOString(),
100
- };
101
- }
102
-
103
- function normalizeArgs(args) {
104
- return Array.isArray(args)
105
- ? args.map((value) => normalizeText(value)).filter(Boolean)
106
- : [];
107
- }
108
-
109
- function normalizeIsoTime(value) {
110
- const normalized = normalizeText(value);
111
- if (!normalized) {
112
- return "";
113
- }
114
- const parsed = Date.parse(normalized);
115
- if (!Number.isFinite(parsed)) {
116
- return "";
117
- }
118
- return new Date(parsed).toISOString();
119
- }
120
-
121
- function compareTimelineScreenshotJobs(left, right) {
122
- const leftTime = Date.parse(left?.createdAt || "") || 0;
123
- const rightTime = Date.parse(right?.createdAt || "") || 0;
124
- if (leftTime !== rightTime) {
125
- return leftTime - rightTime;
126
- }
127
- return String(left?.id || "").localeCompare(String(right?.id || ""));
128
- }
129
-
130
- function normalizeText(value) {
131
- return typeof value === "string" ? value.trim() : "";
132
- }
133
-
134
- module.exports = { TimelineScreenshotQueueStore };
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ class TimelineScreenshotQueueStore {
5
+ constructor({ filePath }) {
6
+ this.filePath = filePath;
7
+ this.state = { jobs: [] };
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 jobs = Array.isArray(parsed?.jobs) ? parsed.jobs : [];
21
+ this.state = {
22
+ jobs: jobs
23
+ .map(normalizeTimelineScreenshotJob)
24
+ .filter(Boolean)
25
+ .sort(compareTimelineScreenshotJobs),
26
+ };
27
+ } catch {
28
+ this.state = { jobs: [] };
29
+ }
30
+ }
31
+
32
+ save() {
33
+ fs.writeFileSync(this.filePath, JSON.stringify(this.state, null, 2));
34
+ }
35
+
36
+ enqueue(job) {
37
+ this.load();
38
+ const normalized = normalizeTimelineScreenshotJob(job);
39
+ if (!normalized) {
40
+ throw new Error("invalid timeline screenshot job");
41
+ }
42
+ this.state.jobs.push(normalized);
43
+ this.state.jobs.sort(compareTimelineScreenshotJobs);
44
+ this.save();
45
+ return normalized;
46
+ }
47
+
48
+ drainForAccount(accountId) {
49
+ this.load();
50
+ const normalizedAccountId = normalizeText(accountId);
51
+ const drained = [];
52
+ const pending = [];
53
+
54
+ for (const job of this.state.jobs) {
55
+ if (job.accountId === normalizedAccountId) {
56
+ drained.push(job);
57
+ } else {
58
+ pending.push(job);
59
+ }
60
+ }
61
+
62
+ if (drained.length) {
63
+ this.state.jobs = pending;
64
+ this.save();
65
+ }
66
+
67
+ return drained;
68
+ }
69
+
70
+ hasPendingForAccount(accountId) {
71
+ this.load();
72
+ const normalizedAccountId = normalizeText(accountId);
73
+ return this.state.jobs.some((job) => job.accountId === normalizedAccountId);
74
+ }
75
+ }
76
+
77
+ function normalizeTimelineScreenshotJob(job) {
78
+ if (!job || typeof job !== "object") {
79
+ return null;
80
+ }
81
+
82
+ const id = normalizeText(job.id);
83
+ const accountId = normalizeText(job.accountId);
84
+ const senderId = normalizeText(job.senderId);
85
+ const outputFile = normalizeText(job.outputFile);
86
+ const createdAt = normalizeIsoTime(job.createdAt);
87
+ const args = normalizeArgs(job.args);
88
+
89
+ if (!id || !accountId || !senderId) {
90
+ return null;
91
+ }
92
+
93
+ return {
94
+ id,
95
+ accountId,
96
+ senderId,
97
+ outputFile,
98
+ args,
99
+ createdAt: createdAt || new Date().toISOString(),
100
+ };
101
+ }
102
+
103
+ function normalizeArgs(args) {
104
+ return Array.isArray(args)
105
+ ? args.map((value) => normalizeText(value)).filter(Boolean)
106
+ : [];
107
+ }
108
+
109
+ function normalizeIsoTime(value) {
110
+ const normalized = normalizeText(value);
111
+ if (!normalized) {
112
+ return "";
113
+ }
114
+ const parsed = Date.parse(normalized);
115
+ if (!Number.isFinite(parsed)) {
116
+ return "";
117
+ }
118
+ return new Date(parsed).toISOString();
119
+ }
120
+
121
+ function compareTimelineScreenshotJobs(left, right) {
122
+ const leftTime = Date.parse(left?.createdAt || "") || 0;
123
+ const rightTime = Date.parse(right?.createdAt || "") || 0;
124
+ if (leftTime !== rightTime) {
125
+ return leftTime - rightTime;
126
+ }
127
+ return String(left?.id || "").localeCompare(String(right?.id || ""));
128
+ }
129
+
130
+ function normalizeText(value) {
131
+ return typeof value === "string" ? value.trim() : "";
132
+ }
133
+
134
+ module.exports = { TimelineScreenshotQueueStore };
@@ -0,0 +1,436 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const LEGACY_TIMELINE_TIMEZONE = "Asia/Shanghai";
5
+ const DEFAULT_FALLBACK_TIMEZONE = "UTC";
6
+
7
+ function normalizeText(value) {
8
+ return typeof value === "string" ? value.trim() : "";
9
+ }
10
+
11
+ function normalizeTimezone(value) {
12
+ const raw = normalizeText(value);
13
+ if (!raw) {
14
+ return "";
15
+ }
16
+ try {
17
+ new Intl.DateTimeFormat("en-US", { timeZone: raw }).format(new Date());
18
+ return raw;
19
+ } catch {
20
+ return "";
21
+ }
22
+ }
23
+
24
+ function resolveSystemTimezone() {
25
+ return normalizeTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
26
+ }
27
+
28
+ function resolveTimezoneConfig({ explicitTimezone = "", timelineStateDir = "" } = {}) {
29
+ const explicit = normalizeTimezone(explicitTimezone);
30
+ const timelineStateTimezone = readTimelineStateTimezone(timelineStateDir);
31
+ if (explicit) {
32
+ return {
33
+ timezone: explicit,
34
+ source: "env",
35
+ explicit: true,
36
+ timelineStateTimezone,
37
+ };
38
+ }
39
+
40
+ // Preserve an explicitly established timeline timezone when it is not just
41
+ // the old hard-coded default. This keeps diary/review/reminder aligned with
42
+ // existing timeline business data instead of silently drifting per machine.
43
+ if (timelineStateTimezone && timelineStateTimezone !== LEGACY_TIMELINE_TIMEZONE) {
44
+ return {
45
+ timezone: timelineStateTimezone,
46
+ source: "timeline_state",
47
+ explicit: false,
48
+ timelineStateTimezone,
49
+ };
50
+ }
51
+
52
+ const systemTimezone = resolveSystemTimezone();
53
+ if (systemTimezone) {
54
+ return {
55
+ timezone: systemTimezone,
56
+ source: "system",
57
+ explicit: false,
58
+ timelineStateTimezone,
59
+ };
60
+ }
61
+
62
+ if (timelineStateTimezone) {
63
+ return {
64
+ timezone: timelineStateTimezone,
65
+ source: "timeline_state_legacy",
66
+ explicit: false,
67
+ timelineStateTimezone,
68
+ };
69
+ }
70
+
71
+ return {
72
+ timezone: DEFAULT_FALLBACK_TIMEZONE,
73
+ source: "fallback",
74
+ explicit: false,
75
+ timelineStateTimezone,
76
+ };
77
+ }
78
+
79
+ function readTimelineStateTimezone(timelineStateDir = "") {
80
+ const snapshot = loadTimelineStateSnapshot(timelineStateDir);
81
+ return snapshot.timezone;
82
+ }
83
+
84
+ function loadTimelineStateSnapshot(timelineStateDir = "") {
85
+ const paths = resolveTimelineStateFiles(timelineStateDir);
86
+ if (!paths.dir) {
87
+ return {
88
+ paths,
89
+ hasAnyFile: false,
90
+ hasFacts: false,
91
+ timezone: "",
92
+ stateDoc: null,
93
+ taxonomyDoc: null,
94
+ factsDoc: null,
95
+ taxonomy: {},
96
+ facts: {},
97
+ proposals: [],
98
+ };
99
+ }
100
+
101
+ const stateDoc = readJsonFile(paths.stateFile);
102
+ const taxonomyDoc = readJsonFile(paths.taxonomyFile);
103
+ const factsDoc = readJsonFile(paths.factsFile);
104
+ const facts = readFacts(stateDoc, factsDoc);
105
+
106
+ return {
107
+ paths,
108
+ hasAnyFile: Boolean(stateDoc || taxonomyDoc || factsDoc),
109
+ hasFacts: Object.keys(facts).length > 0,
110
+ timezone: normalizeTimezone(stateDoc?.timezone)
111
+ || normalizeTimezone(taxonomyDoc?.timezone)
112
+ || normalizeTimezone(factsDoc?.timezone),
113
+ stateDoc,
114
+ taxonomyDoc,
115
+ factsDoc,
116
+ taxonomy: readTaxonomy(stateDoc, taxonomyDoc),
117
+ facts,
118
+ proposals: readProposals(stateDoc, factsDoc),
119
+ };
120
+ }
121
+
122
+ function resolveTimelineStateFiles(timelineStateDir = "") {
123
+ const normalizedDir = normalizeText(timelineStateDir);
124
+ if (!normalizedDir) {
125
+ return {
126
+ dir: "",
127
+ stateFile: "",
128
+ taxonomyFile: "",
129
+ factsFile: "",
130
+ };
131
+ }
132
+
133
+ const baseDir = path.resolve(normalizedDir);
134
+ const nestedDir = path.join(baseDir, "timeline");
135
+ // timeline-for-agent's canonical layout is <stateDir>/timeline/*.json. We
136
+ // still detect legacy direct files for migration, but a fresh root should
137
+ // default to the nested layout so bootstrap, state sync, and the runtime all
138
+ // converge on the same paths.
139
+ const candidates = [nestedDir, baseDir];
140
+ const existingDir = candidates.find(hasAnyTimelineStateFile) || nestedDir;
141
+ return buildTimelineStateFiles(existingDir);
142
+ }
143
+
144
+ function hasAnyTimelineStateFile(dirPath) {
145
+ if (!dirPath) {
146
+ return false;
147
+ }
148
+ const files = buildTimelineStateFiles(dirPath);
149
+ return fs.existsSync(files.stateFile)
150
+ || fs.existsSync(files.taxonomyFile)
151
+ || fs.existsSync(files.factsFile);
152
+ }
153
+
154
+ function buildTimelineStateFiles(dirPath) {
155
+ return {
156
+ dir: dirPath,
157
+ stateFile: path.join(dirPath, "timeline-state.json"),
158
+ taxonomyFile: path.join(dirPath, "timeline-taxonomy.json"),
159
+ factsFile: path.join(dirPath, "timeline-facts.json"),
160
+ };
161
+ }
162
+
163
+ function readTaxonomy(stateDoc, taxonomyDoc) {
164
+ const fromState = stateDoc?.taxonomy;
165
+ if (fromState && typeof fromState === "object") {
166
+ return fromState;
167
+ }
168
+ const fromTaxonomy = taxonomyDoc?.taxonomy;
169
+ return fromTaxonomy && typeof fromTaxonomy === "object" ? fromTaxonomy : {};
170
+ }
171
+
172
+ function readFacts(stateDoc, factsDoc) {
173
+ const fromState = stateDoc?.facts;
174
+ if (fromState && typeof fromState === "object") {
175
+ return fromState;
176
+ }
177
+ const fromFacts = factsDoc?.facts;
178
+ return fromFacts && typeof fromFacts === "object" ? fromFacts : {};
179
+ }
180
+
181
+ function readProposals(stateDoc, factsDoc) {
182
+ if (Array.isArray(stateDoc?.proposals)) {
183
+ return stateDoc.proposals;
184
+ }
185
+ return Array.isArray(factsDoc?.proposals) ? factsDoc.proposals : [];
186
+ }
187
+
188
+ function readJsonFile(filePath) {
189
+ try {
190
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ function formatDateInTimezone(value, timezone = LEGACY_TIMELINE_TIMEZONE) {
197
+ return formatInTimezone(value, timezone, "en-CA", {
198
+ year: "numeric",
199
+ month: "2-digit",
200
+ day: "2-digit",
201
+ });
202
+ }
203
+
204
+ function formatTimeInTimezone(value, timezone = LEGACY_TIMELINE_TIMEZONE, locale = "zh-CN") {
205
+ return formatInTimezone(value, timezone, locale, {
206
+ hour: "2-digit",
207
+ minute: "2-digit",
208
+ hour12: false,
209
+ hourCycle: "h23",
210
+ });
211
+ }
212
+
213
+ function formatDateTimeInTimezone(value, timezone = LEGACY_TIMELINE_TIMEZONE) {
214
+ return formatInTimezone(value, timezone, "sv-SE", {
215
+ year: "numeric",
216
+ month: "2-digit",
217
+ day: "2-digit",
218
+ hour: "2-digit",
219
+ minute: "2-digit",
220
+ hour12: false,
221
+ hourCycle: "h23",
222
+ }).replace(" ", "T");
223
+ }
224
+
225
+ function getCurrentDateStringInTimezone(timezone = LEGACY_TIMELINE_TIMEZONE, now = new Date()) {
226
+ return formatDateInTimezone(now, timezone);
227
+ }
228
+
229
+ function coerceLocalDateTimeToIso(value, {
230
+ timeZone = LEGACY_TIMELINE_TIMEZONE,
231
+ defaultDate = "",
232
+ defaultTime = "",
233
+ } = {}) {
234
+ const normalized = normalizeText(value);
235
+ if (!normalized) {
236
+ return "";
237
+ }
238
+
239
+ if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?([zZ]|[+-]\d{2}:\d{2})$/.test(normalized)) {
240
+ return normalized.replace(" ", "T");
241
+ }
242
+
243
+ const localDateTime = normalized.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}(?::\d{2})?)$/);
244
+ if (localDateTime) {
245
+ return buildZonedIsoString(localDateTime[1], localDateTime[2], timeZone);
246
+ }
247
+
248
+ const localTime = normalized.match(/^(\d{2}:\d{2}(?::\d{2})?)$/);
249
+ if (localTime && normalizeText(defaultDate)) {
250
+ return buildZonedIsoString(defaultDate, localTime[1], timeZone);
251
+ }
252
+
253
+ const localDate = normalized.match(/^(\d{4}-\d{2}-\d{2})$/);
254
+ if (localDate && normalizeText(defaultTime)) {
255
+ return buildZonedIsoString(localDate[1], defaultTime, timeZone);
256
+ }
257
+
258
+ return "";
259
+ }
260
+
261
+ function buildZonedIsoString(dateString, timeString, timeZone = LEGACY_TIMELINE_TIMEZONE) {
262
+ const resolvedTimezone = normalizeTimezone(timeZone) || LEGACY_TIMELINE_TIMEZONE;
263
+ const parts = parseLocalDateTimeParts(dateString, timeString);
264
+ if (!parts) {
265
+ return "";
266
+ }
267
+
268
+ // Convert a wall-clock time in the target timezone into a real instant. We
269
+ // iterate because offsets can change across DST boundaries.
270
+ const instantMs = resolveInstantFromLocalParts(parts, resolvedTimezone);
271
+ const offsetMinutes = getOffsetMinutesForInstant(instantMs, resolvedTimezone);
272
+ return `${formatPartsDate(parts)}T${formatPartsTime(parts)}${formatOffsetMinutes(offsetMinutes)}`;
273
+ }
274
+
275
+ function resolveInstantFromLocalParts(parts, timezone) {
276
+ let guessMs = partsToUtcMs(parts);
277
+ for (let attempt = 0; attempt < 6; attempt += 1) {
278
+ const zonedParts = getLocalDateTimePartsForInstant(guessMs, timezone);
279
+ const deltaMs = partsToUtcMs(parts) - partsToUtcMs(zonedParts);
280
+ if (deltaMs === 0) {
281
+ return guessMs;
282
+ }
283
+ guessMs += deltaMs;
284
+ }
285
+ return guessMs;
286
+ }
287
+
288
+ function getOffsetMinutesForInstant(instantMs, timezone) {
289
+ const zonedParts = getLocalDateTimePartsForInstant(instantMs, timezone);
290
+ return Math.round((partsToUtcMs(zonedParts) - instantMs) / 60000);
291
+ }
292
+
293
+ function getLocalDateTimePartsForInstant(instantMs, timezone) {
294
+ const formatter = new Intl.DateTimeFormat("en-CA", {
295
+ timeZone: normalizeTimezone(timezone) || LEGACY_TIMELINE_TIMEZONE,
296
+ year: "numeric",
297
+ month: "2-digit",
298
+ day: "2-digit",
299
+ hour: "2-digit",
300
+ minute: "2-digit",
301
+ second: "2-digit",
302
+ hour12: false,
303
+ hourCycle: "h23",
304
+ });
305
+ const parts = Object.create(null);
306
+ for (const part of formatter.formatToParts(new Date(instantMs))) {
307
+ if (part.type === "literal") {
308
+ continue;
309
+ }
310
+ parts[part.type] = part.value;
311
+ }
312
+ return {
313
+ year: Number.parseInt(parts.year, 10),
314
+ month: Number.parseInt(parts.month, 10),
315
+ day: Number.parseInt(parts.day, 10),
316
+ hour: Number.parseInt(parts.hour, 10),
317
+ minute: Number.parseInt(parts.minute, 10),
318
+ second: Number.parseInt(parts.second, 10),
319
+ };
320
+ }
321
+
322
+ function parseLocalDateTimeParts(dateString, timeString) {
323
+ const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(normalizeText(dateString));
324
+ const timeMatch = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(normalizeText(timeString));
325
+ if (!dateMatch || !timeMatch) {
326
+ return null;
327
+ }
328
+
329
+ const parts = {
330
+ year: Number.parseInt(dateMatch[1], 10),
331
+ month: Number.parseInt(dateMatch[2], 10),
332
+ day: Number.parseInt(dateMatch[3], 10),
333
+ hour: Number.parseInt(timeMatch[1], 10),
334
+ minute: Number.parseInt(timeMatch[2], 10),
335
+ second: Number.parseInt(timeMatch[3] || "00", 10),
336
+ };
337
+
338
+ if (!isValidDateTimeParts(parts)) {
339
+ return null;
340
+ }
341
+ return parts;
342
+ }
343
+
344
+ function isValidDateTimeParts(parts) {
345
+ if (!Number.isInteger(parts.year) || parts.year < 1) {
346
+ return false;
347
+ }
348
+ if (!Number.isInteger(parts.month) || parts.month < 1 || parts.month > 12) {
349
+ return false;
350
+ }
351
+ if (!Number.isInteger(parts.day) || parts.day < 1 || parts.day > 31) {
352
+ return false;
353
+ }
354
+ if (!Number.isInteger(parts.hour) || parts.hour < 0 || parts.hour > 23) {
355
+ return false;
356
+ }
357
+ if (!Number.isInteger(parts.minute) || parts.minute < 0 || parts.minute > 59) {
358
+ return false;
359
+ }
360
+ if (!Number.isInteger(parts.second) || parts.second < 0 || parts.second > 59) {
361
+ return false;
362
+ }
363
+
364
+ const probe = new Date(Date.UTC(
365
+ parts.year,
366
+ parts.month - 1,
367
+ parts.day,
368
+ parts.hour,
369
+ parts.minute,
370
+ parts.second
371
+ ));
372
+ return probe.getUTCFullYear() === parts.year
373
+ && probe.getUTCMonth() + 1 === parts.month
374
+ && probe.getUTCDate() === parts.day
375
+ && probe.getUTCHours() === parts.hour
376
+ && probe.getUTCMinutes() === parts.minute
377
+ && probe.getUTCSeconds() === parts.second;
378
+ }
379
+
380
+ function partsToUtcMs(parts) {
381
+ return Date.UTC(
382
+ parts.year,
383
+ parts.month - 1,
384
+ parts.day,
385
+ parts.hour,
386
+ parts.minute,
387
+ parts.second || 0
388
+ );
389
+ }
390
+
391
+ function formatPartsDate(parts) {
392
+ return `${String(parts.year).padStart(4, "0")}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}`;
393
+ }
394
+
395
+ function formatPartsTime(parts) {
396
+ return `${String(parts.hour).padStart(2, "0")}:${String(parts.minute).padStart(2, "0")}:${String(parts.second || 0).padStart(2, "0")}`;
397
+ }
398
+
399
+ function formatOffsetMinutes(offsetMinutes) {
400
+ const normalized = Number.isFinite(offsetMinutes) ? offsetMinutes : 0;
401
+ const sign = normalized >= 0 ? "+" : "-";
402
+ const absoluteMinutes = Math.abs(normalized);
403
+ const hours = String(Math.floor(absoluteMinutes / 60)).padStart(2, "0");
404
+ const minutes = String(absoluteMinutes % 60).padStart(2, "0");
405
+ return `${sign}${hours}:${minutes}`;
406
+ }
407
+
408
+ function formatInTimezone(value, timezone, locale, options) {
409
+ const resolvedTimezone = normalizeTimezone(timezone) || LEGACY_TIMELINE_TIMEZONE;
410
+ const date = value instanceof Date ? value : new Date(value);
411
+ if (Number.isNaN(date.getTime())) {
412
+ return "";
413
+ }
414
+ return new Intl.DateTimeFormat(locale, {
415
+ timeZone: resolvedTimezone,
416
+ ...options,
417
+ }).format(date);
418
+ }
419
+
420
+ module.exports = {
421
+ DEFAULT_FALLBACK_TIMEZONE,
422
+ LEGACY_TIMELINE_TIMEZONE,
423
+ buildZonedIsoString,
424
+ coerceLocalDateTimeToIso,
425
+ formatDateInTimezone,
426
+ formatDateTimeInTimezone,
427
+ formatOffsetMinutes,
428
+ formatTimeInTimezone,
429
+ getCurrentDateStringInTimezone,
430
+ loadTimelineStateSnapshot,
431
+ normalizeTimezone,
432
+ readTimelineStateTimezone,
433
+ resolveSystemTimezone,
434
+ resolveTimelineStateFiles,
435
+ resolveTimezoneConfig,
436
+ };