dsh-tdai-memory 0.4.2 → 0.4.3

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 (64) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +4 -0
  3. package/README.zh.md +4 -0
  4. package/THIRD-PARTY-NOTICES.md +20 -20
  5. package/cordis.patch.yml +8 -8
  6. package/package.json +102 -100
  7. package/vendor/tdai/adapters/index.js +15 -15
  8. package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
  9. package/vendor/tdai/adapters/openclaw/index.js +5 -5
  10. package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
  11. package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
  12. package/vendor/tdai/adapters/standalone/index.js +5 -5
  13. package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
  14. package/vendor/tdai/core/hooks/auto-capture.js +236 -236
  15. package/vendor/tdai/core/hooks/auto-recall.js +640 -640
  16. package/vendor/tdai/core/index.js +8 -8
  17. package/vendor/tdai/core/persona/persona-generator.js +184 -184
  18. package/vendor/tdai/core/persona/persona-trigger.js +94 -94
  19. package/vendor/tdai/core/profile/profile-sync.js +200 -200
  20. package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
  21. package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
  22. package/vendor/tdai/core/prompts/persona-generation.js +35 -35
  23. package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
  24. package/vendor/tdai/core/record/l1-dedup.js +318 -318
  25. package/vendor/tdai/core/record/l1-extractor.js +379 -379
  26. package/vendor/tdai/core/record/l1-reader.js +178 -178
  27. package/vendor/tdai/core/record/l1-writer.js +141 -141
  28. package/vendor/tdai/core/report/reporter.js +86 -86
  29. package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
  30. package/vendor/tdai/core/scene/scene-extractor.js +410 -410
  31. package/vendor/tdai/core/scene/scene-format.js +53 -53
  32. package/vendor/tdai/core/scene/scene-index.js +82 -82
  33. package/vendor/tdai/core/scene/scene-navigation.js +57 -57
  34. package/vendor/tdai/core/seed/input.js +404 -404
  35. package/vendor/tdai/core/seed/types.js +9 -9
  36. package/vendor/tdai/core/store/bm25-client.js +117 -117
  37. package/vendor/tdai/core/store/bm25-local.js +68 -68
  38. package/vendor/tdai/core/store/embedding.js +632 -632
  39. package/vendor/tdai/core/store/factory.js +95 -95
  40. package/vendor/tdai/core/store/search-utils.js +54 -54
  41. package/vendor/tdai/core/store/sqlite.js +1632 -1632
  42. package/vendor/tdai/core/store/tcvdb-client.js +221 -221
  43. package/vendor/tdai/core/store/tcvdb.js +1047 -1047
  44. package/vendor/tdai/core/store/types.js +17 -17
  45. package/vendor/tdai/core/tools/conversation-search.js +199 -199
  46. package/vendor/tdai/core/tools/memory-search.js +207 -207
  47. package/vendor/tdai/core/types.js +12 -12
  48. package/vendor/tdai/utils/backup.js +189 -189
  49. package/vendor/tdai/utils/checkpoint.js +360 -360
  50. package/vendor/tdai/utils/clean-context-runner.js +472 -472
  51. package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
  52. package/vendor/tdai/utils/env.js +13 -13
  53. package/vendor/tdai/utils/managed-timer.js +127 -127
  54. package/vendor/tdai/utils/manifest.js +91 -91
  55. package/vendor/tdai/utils/memory-cleaner.js +300 -300
  56. package/vendor/tdai/utils/no-think-fetch.js +109 -109
  57. package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
  58. package/vendor/tdai/utils/pipeline-factory.js +517 -517
  59. package/vendor/tdai/utils/pipeline-manager.js +892 -892
  60. package/vendor/tdai/utils/sanitize.js +360 -360
  61. package/vendor/tdai/utils/serial-queue.js +103 -103
  62. package/vendor/tdai/utils/session-filter.js +88 -88
  63. package/vendor/tdai/utils/text-utils.js +29 -29
  64. package/vendor/tdai/utils/time.js +252 -252
@@ -1,252 +1,252 @@
1
- /**
2
- * Unified time module — the single source of truth for all timezone-aware
3
- * timestamp formatting in the plugin.
4
- *
5
- * Other modules MUST NOT directly call `toISOString()`, `getHours()`, or
6
- * `Intl.DateTimeFormat` for user/LLM-facing timestamps. Import from here instead.
7
- *
8
- * Design: module-level singleton. `initTimeModule()` is called once during
9
- * plugin registration; all subsequent calls read the resolved timezone.
10
- */
11
- // ============================
12
- // Internal state
13
- // ============================
14
- let _resolvedTz = "UTC"; // default, overwritten by initTimeModule()
15
- let _logger;
16
- // ============================
17
- // Initialization
18
- // ============================
19
- /**
20
- * Initialize the time module. Called once during plugin register.
21
- * Subsequent hot-reloads also go through here.
22
- */
23
- export function initTimeModule(cfg, logger) {
24
- _resolvedTz = resolveTimeZone(cfg.timezone, logger);
25
- _logger = logger;
26
- _logger?.debug?.(`[time] Timezone resolved: "${_resolvedTz}"`);
27
- }
28
- /**
29
- * Returns the currently active IANA timezone name (or offset string).
30
- * Useful for diagnostics and prompt generation.
31
- */
32
- export function getActiveTimeZone() {
33
- return _resolvedTz;
34
- }
35
- /**
36
- * @internal test-only — reset to pre-init state.
37
- * Avoids cross-test pollution when vitest runs multiple tests in the same process.
38
- */
39
- export function _resetTimeModuleForTest() {
40
- _resolvedTz = "UTC";
41
- _logger = undefined;
42
- }
43
- // ============================
44
- // A-type: UTC instants (for storage)
45
- // ============================
46
- /**
47
- * Current time as UTC ISO 8601 string with "Z" suffix.
48
- * Used for SQLite/TCVDB timestamps, cursors, and any machine-compared instants.
49
- */
50
- export function nowInstantISO() {
51
- return new Date().toISOString();
52
- }
53
- // ============================
54
- // B-type: Local date/datetime (follows configured tz)
55
- // ============================
56
- /**
57
- * Format a Date as "YYYY-MM-DD" in the configured timezone.
58
- * Used for L0 JSONL shard filenames and cleaner day boundaries.
59
- */
60
- export function formatLocalDate(d = new Date()) {
61
- const parts = new Intl.DateTimeFormat("en-CA", {
62
- timeZone: _resolvedTz,
63
- year: "numeric",
64
- month: "2-digit",
65
- day: "2-digit",
66
- }).formatToParts(d);
67
- const year = parts.find((p) => p.type === "year").value;
68
- const month = parts.find((p) => p.type === "month").value;
69
- const day = parts.find((p) => p.type === "day").value;
70
- return `${year}-${month}-${day}`;
71
- }
72
- /**
73
- * Format a Date as "YYYY-MM-DD HH:mm:ss" in the configured timezone.
74
- * Used for cleaner audit logs and human-readable local timestamps.
75
- */
76
- export function formatLocalDateTime(d = new Date()) {
77
- const parts = new Intl.DateTimeFormat("en-CA", {
78
- timeZone: _resolvedTz,
79
- year: "numeric",
80
- month: "2-digit",
81
- day: "2-digit",
82
- hour: "2-digit",
83
- minute: "2-digit",
84
- second: "2-digit",
85
- hour12: false,
86
- }).formatToParts(d);
87
- const get = (type) => parts.find((p) => p.type === type).value;
88
- return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}:${get("second")}`;
89
- }
90
- /**
91
- * Compute the start-of-day (00:00:00.000) in the configured timezone for a given date.
92
- * Returns a UTC millisecond timestamp.
93
- * Used by memory-cleaner for cutoff calculations.
94
- */
95
- export function startOfLocalDay(d = new Date()) {
96
- // Get the local date components in the configured timezone
97
- const dateStr = formatLocalDate(d);
98
- // Parse as midnight in the configured timezone
99
- // Use a trick: format "YYYY-MM-DDT00:00:00" and find the UTC equivalent
100
- const midnightLocal = new Date(`${dateStr}T00:00:00`);
101
- // We need to find the UTC instant that corresponds to midnight in _resolvedTz.
102
- // Approach: binary search isn't needed — we can use the timezone offset.
103
- const formatter = new Intl.DateTimeFormat("en-US", {
104
- timeZone: _resolvedTz,
105
- year: "numeric",
106
- month: "2-digit",
107
- day: "2-digit",
108
- hour: "2-digit",
109
- minute: "2-digit",
110
- second: "2-digit",
111
- hour12: false,
112
- });
113
- // Start with an estimate: the date at UTC midnight
114
- const utcMidnight = new Date(`${dateStr}T00:00:00Z`);
115
- // Check what local time that corresponds to
116
- const localParts = formatter.formatToParts(utcMidnight);
117
- const localHour = parseInt(localParts.find((p) => p.type === "hour").value, 10);
118
- const localMinute = parseInt(localParts.find((p) => p.type === "minute").value, 10);
119
- const localDay = localParts.find((p) => p.type === "day").value;
120
- const targetDay = dateStr.slice(8, 10); // DD from YYYY-MM-DD
121
- // The offset from UTC to local is: if UTC midnight shows as localHour:localMinute on localDay
122
- // then local midnight = UTC midnight - (localHour*60 + localMinute) minutes
123
- // But we need to handle day boundary crossings
124
- let offsetMinutes = localHour * 60 + localMinute;
125
- if (localDay !== targetDay) {
126
- // Day wrapped — means local is behind UTC (negative offset zones)
127
- // e.g. UTC midnight = previous day 19:00 in America/New_York
128
- offsetMinutes = offsetMinutes - 24 * 60;
129
- }
130
- // Local midnight in UTC = UTC midnight - offset
131
- return utcMidnight.getTime() - offsetMinutes * 60 * 1000;
132
- }
133
- // ============================
134
- // C-type: LLM-facing timestamps (ISO 8601 with offset)
135
- // ============================
136
- /**
137
- * Format a timestamp for LLM consumption: ISO 8601 with explicit UTC offset.
138
- * Example: "2026-04-07T11:04:45+08:00"
139
- *
140
- * Handles:
141
- * - Date objects
142
- * - ISO 8601 strings (with or without "Z")
143
- * - Unix millisecond timestamps (numbers)
144
- *
145
- * Old UTC data ("Z" suffix) is correctly converted to the configured timezone.
146
- */
147
- export function formatForLLM(input) {
148
- const d = input instanceof Date ? input : new Date(input);
149
- if (isNaN(d.getTime())) {
150
- return String(input); // pass-through for unparseable values
151
- }
152
- // Get components in the configured timezone
153
- const parts = new Intl.DateTimeFormat("en-CA", {
154
- timeZone: _resolvedTz,
155
- year: "numeric",
156
- month: "2-digit",
157
- day: "2-digit",
158
- hour: "2-digit",
159
- minute: "2-digit",
160
- second: "2-digit",
161
- hour12: false,
162
- }).formatToParts(d);
163
- const get = (type) => parts.find((p) => p.type === type).value;
164
- const dateTime = `${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}`;
165
- // Compute UTC offset for this instant in the configured timezone
166
- const offset = getUtcOffset(d);
167
- return `${dateTime}${offset}`;
168
- }
169
- /**
170
- * Generate a timezone declaration string for system prompts.
171
- * Example: "All timestamps below are in Asia/Shanghai (UTC+08:00). When reasoning about time, use this timezone."
172
- */
173
- export function describeTimeZoneForPrompt() {
174
- const offset = getUtcOffset(new Date());
175
- return `All timestamps below are in ${_resolvedTz} (UTC${offset}). When reasoning about "yesterday", "last week", or time differences, use this timezone.`;
176
- }
177
- // ============================
178
- // Internal helpers
179
- // ============================
180
- /**
181
- * Resolve the timezone configuration string to a validated timezone identifier.
182
- *
183
- * Accepts:
184
- * - "system" or undefined → process system timezone
185
- * - IANA names: "Asia/Shanghai", "Europe/Berlin", "UTC"
186
- * - UTC offset strings: "+08:00", "-05:30" (ECMA-402 2024)
187
- *
188
- * Invalid values fall back to system timezone with a warning.
189
- */
190
- function resolveTimeZone(cfg, logger) {
191
- if (!cfg || cfg === "system") {
192
- return Intl.DateTimeFormat().resolvedOptions().timeZone;
193
- }
194
- // Node 22+ Intl natively supports IANA names and UTC offset strings
195
- // per ECMA-402 2024 — no manual regex/Etc/GMT conversion needed.
196
- if (validateTimeZone(cfg))
197
- return cfg;
198
- logger?.warn?.(`[time] Invalid timezone "${cfg}", falling back to system timezone`);
199
- return Intl.DateTimeFormat().resolvedOptions().timeZone;
200
- }
201
- /**
202
- * Validate a timezone string using the Intl API.
203
- * Works for IANA names and UTC offset strings ("+05:30", "-08:00").
204
- */
205
- function validateTimeZone(tz) {
206
- try {
207
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
208
- return true;
209
- }
210
- catch {
211
- return false;
212
- }
213
- }
214
- /**
215
- * Compute the UTC offset string (e.g. "+08:00", "-05:30", "+00:00")
216
- * for a given instant in the configured timezone.
217
- */
218
- function getUtcOffset(d) {
219
- // Strategy: compare the "wall clock" time in the target tz vs UTC
220
- // to derive the offset for this specific instant (handles DST).
221
- const utcParts = new Intl.DateTimeFormat("en-CA", {
222
- timeZone: "UTC",
223
- year: "numeric",
224
- month: "2-digit",
225
- day: "2-digit",
226
- hour: "2-digit",
227
- minute: "2-digit",
228
- hour12: false,
229
- }).formatToParts(d);
230
- const localParts = new Intl.DateTimeFormat("en-CA", {
231
- timeZone: _resolvedTz,
232
- year: "numeric",
233
- month: "2-digit",
234
- day: "2-digit",
235
- hour: "2-digit",
236
- minute: "2-digit",
237
- hour12: false,
238
- }).formatToParts(d);
239
- const toMinutes = (parts) => {
240
- const get = (type) => parseInt(parts.find((p) => p.type === type).value, 10);
241
- const y = get("year"), mo = get("month"), day = get("day");
242
- const h = get("hour"), mi = get("minute");
243
- // Convert to a comparable minute-of-epoch (approximate, good enough for offset calc)
244
- return ((y * 12 + mo) * 31 + day) * 24 * 60 + h * 60 + mi;
245
- };
246
- const diffMinutes = toMinutes(localParts) - toMinutes(utcParts);
247
- const sign = diffMinutes >= 0 ? "+" : "-";
248
- const abs = Math.abs(diffMinutes);
249
- const hh = String(Math.floor(abs / 60)).padStart(2, "0");
250
- const mm = String(abs % 60).padStart(2, "0");
251
- return `${sign}${hh}:${mm}`;
252
- }
1
+ /**
2
+ * Unified time module — the single source of truth for all timezone-aware
3
+ * timestamp formatting in the plugin.
4
+ *
5
+ * Other modules MUST NOT directly call `toISOString()`, `getHours()`, or
6
+ * `Intl.DateTimeFormat` for user/LLM-facing timestamps. Import from here instead.
7
+ *
8
+ * Design: module-level singleton. `initTimeModule()` is called once during
9
+ * plugin registration; all subsequent calls read the resolved timezone.
10
+ */
11
+ // ============================
12
+ // Internal state
13
+ // ============================
14
+ let _resolvedTz = "UTC"; // default, overwritten by initTimeModule()
15
+ let _logger;
16
+ // ============================
17
+ // Initialization
18
+ // ============================
19
+ /**
20
+ * Initialize the time module. Called once during plugin register.
21
+ * Subsequent hot-reloads also go through here.
22
+ */
23
+ export function initTimeModule(cfg, logger) {
24
+ _resolvedTz = resolveTimeZone(cfg.timezone, logger);
25
+ _logger = logger;
26
+ _logger?.debug?.(`[time] Timezone resolved: "${_resolvedTz}"`);
27
+ }
28
+ /**
29
+ * Returns the currently active IANA timezone name (or offset string).
30
+ * Useful for diagnostics and prompt generation.
31
+ */
32
+ export function getActiveTimeZone() {
33
+ return _resolvedTz;
34
+ }
35
+ /**
36
+ * @internal test-only — reset to pre-init state.
37
+ * Avoids cross-test pollution when vitest runs multiple tests in the same process.
38
+ */
39
+ export function _resetTimeModuleForTest() {
40
+ _resolvedTz = "UTC";
41
+ _logger = undefined;
42
+ }
43
+ // ============================
44
+ // A-type: UTC instants (for storage)
45
+ // ============================
46
+ /**
47
+ * Current time as UTC ISO 8601 string with "Z" suffix.
48
+ * Used for SQLite/TCVDB timestamps, cursors, and any machine-compared instants.
49
+ */
50
+ export function nowInstantISO() {
51
+ return new Date().toISOString();
52
+ }
53
+ // ============================
54
+ // B-type: Local date/datetime (follows configured tz)
55
+ // ============================
56
+ /**
57
+ * Format a Date as "YYYY-MM-DD" in the configured timezone.
58
+ * Used for L0 JSONL shard filenames and cleaner day boundaries.
59
+ */
60
+ export function formatLocalDate(d = new Date()) {
61
+ const parts = new Intl.DateTimeFormat("en-CA", {
62
+ timeZone: _resolvedTz,
63
+ year: "numeric",
64
+ month: "2-digit",
65
+ day: "2-digit",
66
+ }).formatToParts(d);
67
+ const year = parts.find((p) => p.type === "year").value;
68
+ const month = parts.find((p) => p.type === "month").value;
69
+ const day = parts.find((p) => p.type === "day").value;
70
+ return `${year}-${month}-${day}`;
71
+ }
72
+ /**
73
+ * Format a Date as "YYYY-MM-DD HH:mm:ss" in the configured timezone.
74
+ * Used for cleaner audit logs and human-readable local timestamps.
75
+ */
76
+ export function formatLocalDateTime(d = new Date()) {
77
+ const parts = new Intl.DateTimeFormat("en-CA", {
78
+ timeZone: _resolvedTz,
79
+ year: "numeric",
80
+ month: "2-digit",
81
+ day: "2-digit",
82
+ hour: "2-digit",
83
+ minute: "2-digit",
84
+ second: "2-digit",
85
+ hour12: false,
86
+ }).formatToParts(d);
87
+ const get = (type) => parts.find((p) => p.type === type).value;
88
+ return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}:${get("second")}`;
89
+ }
90
+ /**
91
+ * Compute the start-of-day (00:00:00.000) in the configured timezone for a given date.
92
+ * Returns a UTC millisecond timestamp.
93
+ * Used by memory-cleaner for cutoff calculations.
94
+ */
95
+ export function startOfLocalDay(d = new Date()) {
96
+ // Get the local date components in the configured timezone
97
+ const dateStr = formatLocalDate(d);
98
+ // Parse as midnight in the configured timezone
99
+ // Use a trick: format "YYYY-MM-DDT00:00:00" and find the UTC equivalent
100
+ const midnightLocal = new Date(`${dateStr}T00:00:00`);
101
+ // We need to find the UTC instant that corresponds to midnight in _resolvedTz.
102
+ // Approach: binary search isn't needed — we can use the timezone offset.
103
+ const formatter = new Intl.DateTimeFormat("en-US", {
104
+ timeZone: _resolvedTz,
105
+ year: "numeric",
106
+ month: "2-digit",
107
+ day: "2-digit",
108
+ hour: "2-digit",
109
+ minute: "2-digit",
110
+ second: "2-digit",
111
+ hour12: false,
112
+ });
113
+ // Start with an estimate: the date at UTC midnight
114
+ const utcMidnight = new Date(`${dateStr}T00:00:00Z`);
115
+ // Check what local time that corresponds to
116
+ const localParts = formatter.formatToParts(utcMidnight);
117
+ const localHour = parseInt(localParts.find((p) => p.type === "hour").value, 10);
118
+ const localMinute = parseInt(localParts.find((p) => p.type === "minute").value, 10);
119
+ const localDay = localParts.find((p) => p.type === "day").value;
120
+ const targetDay = dateStr.slice(8, 10); // DD from YYYY-MM-DD
121
+ // The offset from UTC to local is: if UTC midnight shows as localHour:localMinute on localDay
122
+ // then local midnight = UTC midnight - (localHour*60 + localMinute) minutes
123
+ // But we need to handle day boundary crossings
124
+ let offsetMinutes = localHour * 60 + localMinute;
125
+ if (localDay !== targetDay) {
126
+ // Day wrapped — means local is behind UTC (negative offset zones)
127
+ // e.g. UTC midnight = previous day 19:00 in America/New_York
128
+ offsetMinutes = offsetMinutes - 24 * 60;
129
+ }
130
+ // Local midnight in UTC = UTC midnight - offset
131
+ return utcMidnight.getTime() - offsetMinutes * 60 * 1000;
132
+ }
133
+ // ============================
134
+ // C-type: LLM-facing timestamps (ISO 8601 with offset)
135
+ // ============================
136
+ /**
137
+ * Format a timestamp for LLM consumption: ISO 8601 with explicit UTC offset.
138
+ * Example: "2026-04-07T11:04:45+08:00"
139
+ *
140
+ * Handles:
141
+ * - Date objects
142
+ * - ISO 8601 strings (with or without "Z")
143
+ * - Unix millisecond timestamps (numbers)
144
+ *
145
+ * Old UTC data ("Z" suffix) is correctly converted to the configured timezone.
146
+ */
147
+ export function formatForLLM(input) {
148
+ const d = input instanceof Date ? input : new Date(input);
149
+ if (isNaN(d.getTime())) {
150
+ return String(input); // pass-through for unparseable values
151
+ }
152
+ // Get components in the configured timezone
153
+ const parts = new Intl.DateTimeFormat("en-CA", {
154
+ timeZone: _resolvedTz,
155
+ year: "numeric",
156
+ month: "2-digit",
157
+ day: "2-digit",
158
+ hour: "2-digit",
159
+ minute: "2-digit",
160
+ second: "2-digit",
161
+ hour12: false,
162
+ }).formatToParts(d);
163
+ const get = (type) => parts.find((p) => p.type === type).value;
164
+ const dateTime = `${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}`;
165
+ // Compute UTC offset for this instant in the configured timezone
166
+ const offset = getUtcOffset(d);
167
+ return `${dateTime}${offset}`;
168
+ }
169
+ /**
170
+ * Generate a timezone declaration string for system prompts.
171
+ * Example: "All timestamps below are in Asia/Shanghai (UTC+08:00). When reasoning about time, use this timezone."
172
+ */
173
+ export function describeTimeZoneForPrompt() {
174
+ const offset = getUtcOffset(new Date());
175
+ return `All timestamps below are in ${_resolvedTz} (UTC${offset}). When reasoning about "yesterday", "last week", or time differences, use this timezone.`;
176
+ }
177
+ // ============================
178
+ // Internal helpers
179
+ // ============================
180
+ /**
181
+ * Resolve the timezone configuration string to a validated timezone identifier.
182
+ *
183
+ * Accepts:
184
+ * - "system" or undefined → process system timezone
185
+ * - IANA names: "Asia/Shanghai", "Europe/Berlin", "UTC"
186
+ * - UTC offset strings: "+08:00", "-05:30" (ECMA-402 2024)
187
+ *
188
+ * Invalid values fall back to system timezone with a warning.
189
+ */
190
+ function resolveTimeZone(cfg, logger) {
191
+ if (!cfg || cfg === "system") {
192
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
193
+ }
194
+ // Node 22+ Intl natively supports IANA names and UTC offset strings
195
+ // per ECMA-402 2024 — no manual regex/Etc/GMT conversion needed.
196
+ if (validateTimeZone(cfg))
197
+ return cfg;
198
+ logger?.warn?.(`[time] Invalid timezone "${cfg}", falling back to system timezone`);
199
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
200
+ }
201
+ /**
202
+ * Validate a timezone string using the Intl API.
203
+ * Works for IANA names and UTC offset strings ("+05:30", "-08:00").
204
+ */
205
+ function validateTimeZone(tz) {
206
+ try {
207
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
208
+ return true;
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ }
214
+ /**
215
+ * Compute the UTC offset string (e.g. "+08:00", "-05:30", "+00:00")
216
+ * for a given instant in the configured timezone.
217
+ */
218
+ function getUtcOffset(d) {
219
+ // Strategy: compare the "wall clock" time in the target tz vs UTC
220
+ // to derive the offset for this specific instant (handles DST).
221
+ const utcParts = new Intl.DateTimeFormat("en-CA", {
222
+ timeZone: "UTC",
223
+ year: "numeric",
224
+ month: "2-digit",
225
+ day: "2-digit",
226
+ hour: "2-digit",
227
+ minute: "2-digit",
228
+ hour12: false,
229
+ }).formatToParts(d);
230
+ const localParts = new Intl.DateTimeFormat("en-CA", {
231
+ timeZone: _resolvedTz,
232
+ year: "numeric",
233
+ month: "2-digit",
234
+ day: "2-digit",
235
+ hour: "2-digit",
236
+ minute: "2-digit",
237
+ hour12: false,
238
+ }).formatToParts(d);
239
+ const toMinutes = (parts) => {
240
+ const get = (type) => parseInt(parts.find((p) => p.type === type).value, 10);
241
+ const y = get("year"), mo = get("month"), day = get("day");
242
+ const h = get("hour"), mi = get("minute");
243
+ // Convert to a comparable minute-of-epoch (approximate, good enough for offset calc)
244
+ return ((y * 12 + mo) * 31 + day) * 24 * 60 + h * 60 + mi;
245
+ };
246
+ const diffMinutes = toMinutes(localParts) - toMinutes(utcParts);
247
+ const sign = diffMinutes >= 0 ? "+" : "-";
248
+ const abs = Math.abs(diffMinutes);
249
+ const hh = String(Math.floor(abs / 60)).padStart(2, "0");
250
+ const mm = String(abs % 60).padStart(2, "0");
251
+ return `${sign}${hh}:${mm}`;
252
+ }