dsh-tdai-memory 0.2.10 → 0.2.12
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/LICENSE +21 -21
- package/README.md +137 -129
- package/README.zh.md +75 -70
- package/THIRD-PARTY-NOTICES.md +20 -20
- package/client.js +350 -349
- package/cordis.patch.yml +8 -8
- package/index.js +414 -421
- package/package.json +17 -10
- package/recall-inject.js +86 -86
- package/vendor/tdai/adapters/index.js +15 -15
- package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
- package/vendor/tdai/adapters/openclaw/index.js +5 -5
- package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
- package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
- package/vendor/tdai/adapters/standalone/index.js +5 -5
- package/vendor/tdai/adapters/standalone/llm-runner.js +246 -246
- package/vendor/tdai/config.js +363 -363
- package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
- package/vendor/tdai/core/hooks/auto-capture.js +236 -236
- package/vendor/tdai/core/hooks/auto-recall.js +640 -640
- package/vendor/tdai/core/index.js +8 -8
- package/vendor/tdai/core/persona/persona-generator.js +184 -184
- package/vendor/tdai/core/persona/persona-trigger.js +94 -94
- package/vendor/tdai/core/profile/profile-sync.js +200 -200
- package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
- package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
- package/vendor/tdai/core/prompts/persona-generation.js +35 -35
- package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
- package/vendor/tdai/core/record/l1-dedup.js +318 -318
- package/vendor/tdai/core/record/l1-extractor.js +379 -379
- package/vendor/tdai/core/record/l1-reader.js +178 -178
- package/vendor/tdai/core/record/l1-writer.js +141 -141
- package/vendor/tdai/core/report/reporter.js +86 -86
- package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
- package/vendor/tdai/core/scene/scene-extractor.js +410 -410
- package/vendor/tdai/core/scene/scene-format.js +53 -53
- package/vendor/tdai/core/scene/scene-index.js +82 -82
- package/vendor/tdai/core/scene/scene-navigation.js +57 -57
- package/vendor/tdai/core/seed/input.js +404 -404
- package/vendor/tdai/core/seed/seed-runtime.js +314 -314
- package/vendor/tdai/core/seed/types.js +9 -9
- package/vendor/tdai/core/store/bm25-client.js +117 -117
- package/vendor/tdai/core/store/bm25-local.js +68 -68
- package/vendor/tdai/core/store/embedding.js +632 -632
- package/vendor/tdai/core/store/factory.js +95 -95
- package/vendor/tdai/core/store/search-utils.js +54 -54
- package/vendor/tdai/core/store/sqlite.js +1632 -1632
- package/vendor/tdai/core/store/tcvdb-client.js +221 -221
- package/vendor/tdai/core/store/tcvdb.js +1047 -1047
- package/vendor/tdai/core/store/types.js +17 -17
- package/vendor/tdai/core/tdai-core.js +446 -446
- package/vendor/tdai/core/tools/conversation-search.js +199 -199
- package/vendor/tdai/core/tools/memory-search.js +207 -207
- package/vendor/tdai/core/types.js +12 -12
- package/vendor/tdai/utils/backup.js +189 -189
- package/vendor/tdai/utils/checkpoint.js +360 -360
- package/vendor/tdai/utils/clean-context-runner.js +472 -472
- package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
- package/vendor/tdai/utils/env.js +13 -13
- package/vendor/tdai/utils/managed-timer.js +127 -127
- package/vendor/tdai/utils/manifest.js +91 -91
- package/vendor/tdai/utils/memory-cleaner.js +300 -300
- package/vendor/tdai/utils/no-think-fetch.js +109 -109
- package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
- package/vendor/tdai/utils/pipeline-factory.js +517 -517
- package/vendor/tdai/utils/pipeline-manager.js +892 -892
- package/vendor/tdai/utils/sanitize.js +360 -360
- package/vendor/tdai/utils/serial-queue.js +103 -103
- package/vendor/tdai/utils/session-filter.js +88 -88
- package/vendor/tdai/utils/text-utils.js +29 -29
- package/vendor/tdai/utils/time.js +252 -252
- package/vendor/dsh-settings-expose.js +0 -75
|
@@ -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
|
+
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-host-apiproxy exposes only namespaces listed in its hard-coded
|
|
3
|
-
* WEB_SETTINGS_NAMESPACES allowlist to the Web settings client. A namespace
|
|
4
|
-
* registered by a third-party plugin answers `settings-not-exposed` even
|
|
5
|
-
* though it is registered — upstream explicitly defers letting plugins
|
|
6
|
-
* expose their own configuration.
|
|
7
|
-
*
|
|
8
|
-
* This module idempotently patches that allowlist in the dsh installation
|
|
9
|
-
* actually loaded by the host process, so the plugin's settings section
|
|
10
|
-
* becomes visible in the Web UI without manual edits. A dsh update
|
|
11
|
-
* overwrites the file; the next plugin start re-patches it.
|
|
12
|
-
*/
|
|
13
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
-
import { createRequire } from "node:module";
|
|
15
|
-
import { dirname, join, sep } from "node:path";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Ensure `nsName` is present in dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES.
|
|
19
|
-
* No-op when already exposed or when the file cannot be located/patched.
|
|
20
|
-
* @param {import("cordis").Context} ctx
|
|
21
|
-
* @param {string} nsName - settings namespace short name (e.g. "tdai-memory").
|
|
22
|
-
* @param {{info?: Function, warn?: Function}} logger - dsh logger.
|
|
23
|
-
*/
|
|
24
|
-
export function ensureSettingsNamespaceExposed(ctx, nsName, logger) {
|
|
25
|
-
void ctx;
|
|
26
|
-
try {
|
|
27
|
-
const target = findApiproxyIndex();
|
|
28
|
-
if (!target) {
|
|
29
|
-
logger?.warn?.(`[settings-expose] could not locate dsh-host-apiproxy; add "${nsName}" to WEB_SETTINGS_NAMESPACES in dsh-host-apiproxy/lib/index.js to get a Web settings section`);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
let src;
|
|
33
|
-
try {
|
|
34
|
-
src = readFileSync(target, "utf8");
|
|
35
|
-
} catch (error) {
|
|
36
|
-
logger?.warn?.(`[settings-expose] cannot read ${target}: ${String(error)}`);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const body = src.match(/const WEB_SETTINGS_NAMESPACES = \[([\s\S]*?)\];/)?.[1] ?? "";
|
|
40
|
-
if (body.includes(`"${nsName}"`)) return; // already exposed (manual or previous patch)
|
|
41
|
-
const patched = src.replace(/(const WEB_SETTINGS_NAMESPACES = \[[\s\S]*?)(\n\s*\];)/, (_, pre, post) => {
|
|
42
|
-
const trailingComma = /,\s*$/.test(pre) ? "" : ",";
|
|
43
|
-
const sep2 = pre.trimEnd().endsWith("[") ? "" : trailingComma;
|
|
44
|
-
return `${pre}${sep2}\n\t"${nsName}"${post}`;
|
|
45
|
-
});
|
|
46
|
-
if (patched === src) {
|
|
47
|
-
logger?.warn?.(`[settings-expose] allowlist pattern not found in ${target}; add "${nsName}" to WEB_SETTINGS_NAMESPACES manually`);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
writeFileSync(target, patched, "utf8");
|
|
51
|
-
logger?.info?.(`[settings-expose] added "${nsName}" to WEB_SETTINGS_NAMESPACES (${target}) — restart dsh web for the settings section to appear`);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
logger?.warn?.(`[settings-expose] failed: ${String(error)}`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function findApiproxyIndex() {
|
|
58
|
-
// 1) The host process has already loaded dsh-host-apiproxy: read the real
|
|
59
|
-
// module path from the CommonJS module cache (any install layout).
|
|
60
|
-
try {
|
|
61
|
-
const Module = createRequire(import.meta.url)("module");
|
|
62
|
-
const cache = Module._cache ?? {};
|
|
63
|
-
for (const key of Object.keys(cache)) {
|
|
64
|
-
if (key.includes(`${sep}dsh-host-apiproxy${sep}`) && key.endsWith(`${sep}index.js`)) return key;
|
|
65
|
-
}
|
|
66
|
-
} catch { /* fall through */ }
|
|
67
|
-
// 2) Fallback: sibling of @deepseek-ai/dsh-settings (dsh's nested layout).
|
|
68
|
-
try {
|
|
69
|
-
const require = createRequire(import.meta.url);
|
|
70
|
-
const settingsEntry = require.resolve("@deepseek-ai/dsh-settings");
|
|
71
|
-
const candidate = join(dirname(dirname(dirname(settingsEntry))), "dsh-host-apiproxy", "lib", "index.js");
|
|
72
|
-
if (existsSync(candidate)) return candidate;
|
|
73
|
-
} catch { /* fall through */ }
|
|
74
|
-
return "";
|
|
75
|
-
}
|