decant-core 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ai/claude.js +46 -1
- package/ai/deepseek.js +7 -0
- package/ai/meta.js +14 -2
- package/package.json +1 -1
- package/utils/timestamps.js +65 -0
package/ai/claude.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChatParser } from "./base.js";
|
|
2
2
|
import { convertToMarkdown } from "../utils/html-to-markdown.js";
|
|
3
|
+
import { pickTimestamp } from "../utils/timestamps.js";
|
|
3
4
|
|
|
4
5
|
async function getOrganizationId() {
|
|
5
6
|
try {
|
|
@@ -736,6 +737,13 @@ export class ClaudeParser extends ChatParser {
|
|
|
736
737
|
if (thinkingStr) {
|
|
737
738
|
msgObj.thinking = thinkingStr;
|
|
738
739
|
}
|
|
740
|
+
const timestamp = pickTimestamp(message, [
|
|
741
|
+
"created_at",
|
|
742
|
+
"updated_at",
|
|
743
|
+
]);
|
|
744
|
+
if (timestamp) {
|
|
745
|
+
msgObj.timestamp = timestamp;
|
|
746
|
+
}
|
|
739
747
|
messages.push(msgObj);
|
|
740
748
|
}
|
|
741
749
|
|
|
@@ -760,6 +768,14 @@ export class ClaudeParser extends ChatParser {
|
|
|
760
768
|
messages.push({
|
|
761
769
|
role: "Claude Artifact",
|
|
762
770
|
content: artContent.trim(),
|
|
771
|
+
...(pickTimestamp(message, ["created_at", "updated_at"])
|
|
772
|
+
? {
|
|
773
|
+
timestamp: pickTimestamp(message, [
|
|
774
|
+
"created_at",
|
|
775
|
+
"updated_at",
|
|
776
|
+
]),
|
|
777
|
+
}
|
|
778
|
+
: {}),
|
|
763
779
|
});
|
|
764
780
|
}
|
|
765
781
|
}
|
|
@@ -912,7 +928,36 @@ export class ClaudeParser extends ChatParser {
|
|
|
912
928
|
}
|
|
913
929
|
|
|
914
930
|
if (content) {
|
|
915
|
-
|
|
931
|
+
const msgObj = { role, content };
|
|
932
|
+
// Best-effort DOM timestamp. <time datetime> usually lives in the
|
|
933
|
+
// sibling MessageActions toolbar inside the same transcript row —
|
|
934
|
+
// not inside the message element itself. Scope to the closest row
|
|
935
|
+
// so we never borrow the previous/next turn's timestamp. Sparse in
|
|
936
|
+
// practice (hover-only on some turns) — absent stays absent.
|
|
937
|
+
try {
|
|
938
|
+
let timeEl =
|
|
939
|
+
typeof el.querySelector === "function"
|
|
940
|
+
? el.querySelector("time[datetime]")
|
|
941
|
+
: null;
|
|
942
|
+
if (!timeEl && typeof el.closest === "function") {
|
|
943
|
+
// Narrow containers only: transcript-row holds one turn, so its
|
|
944
|
+
// time belongs to this message. Never fall back to broad
|
|
945
|
+
// containers like article (many turns) — wrong date is worse
|
|
946
|
+
// than no date.
|
|
947
|
+
const row = el.closest(
|
|
948
|
+
'[data-testid="transcript-row"], .group\\/message-row',
|
|
949
|
+
);
|
|
950
|
+
timeEl = row?.querySelector?.("time[datetime]") || null;
|
|
951
|
+
}
|
|
952
|
+
const datetime = timeEl?.getAttribute?.("datetime");
|
|
953
|
+
const timestamp = pickTimestamp({ datetime }, ["datetime"]);
|
|
954
|
+
if (timestamp) {
|
|
955
|
+
msgObj.timestamp = timestamp;
|
|
956
|
+
}
|
|
957
|
+
} catch {
|
|
958
|
+
// Ignore DOM timestamp lookup errors
|
|
959
|
+
}
|
|
960
|
+
messages.push(msgObj);
|
|
916
961
|
}
|
|
917
962
|
}
|
|
918
963
|
|
package/ai/deepseek.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChatParser } from "./base.js";
|
|
2
2
|
import { convertToMarkdown } from "../utils/html-to-markdown.js";
|
|
3
|
+
import { normalizeTimestamp } from "../utils/timestamps.js";
|
|
3
4
|
|
|
4
5
|
function getUserToken() {
|
|
5
6
|
try {
|
|
@@ -120,6 +121,12 @@ async function fetchDeepSeekConversation(sessionId, token) {
|
|
|
120
121
|
if (thinking) {
|
|
121
122
|
msg.thinking = thinking;
|
|
122
123
|
}
|
|
124
|
+
// Turn granularity: inserted_at is seconds-epoch, ~identical within a
|
|
125
|
+
// USER/ASSISTANT pair, so both sides share the turn's timestamp.
|
|
126
|
+
const timestamp = normalizeTimestamp(msgNode?.inserted_at);
|
|
127
|
+
if (timestamp) {
|
|
128
|
+
msg.timestamp = timestamp;
|
|
129
|
+
}
|
|
123
130
|
return msg;
|
|
124
131
|
})
|
|
125
132
|
.filter((msg) => msg.content.length > 0);
|
package/ai/meta.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChatParser } from "./base.js";
|
|
2
2
|
import { convertToMarkdown } from "../utils/html-to-markdown.js";
|
|
3
|
+
import { pickTimestamp } from "../utils/timestamps.js";
|
|
3
4
|
|
|
4
5
|
// Observed Relay doc_ids for the conversation message list (Sept 2026).
|
|
5
6
|
// These rotate when Meta redeploys the web client. The parser tries the
|
|
@@ -139,12 +140,23 @@ export function formatMetaEdges(edges) {
|
|
|
139
140
|
});
|
|
140
141
|
const messages = [];
|
|
141
142
|
for (const node of nodes) {
|
|
143
|
+
// Turn granularity: user + assistant in the same turn share createdAt,
|
|
144
|
+
// so both messages carry the same timestamp (matches Perplexity pattern).
|
|
145
|
+
const timestamp = pickTimestamp(node, ["createdAt", "userCreatedAt"]);
|
|
142
146
|
if (node.__typename === "UserMessage") {
|
|
143
147
|
const content = extractMetaUserText(node);
|
|
144
|
-
if (content)
|
|
148
|
+
if (content) {
|
|
149
|
+
const msg = { role: "User", content };
|
|
150
|
+
if (timestamp) msg.timestamp = timestamp;
|
|
151
|
+
messages.push(msg);
|
|
152
|
+
}
|
|
145
153
|
} else if (node.__typename === "AssistantMessage") {
|
|
146
154
|
const content = extractMetaAssistantText(node);
|
|
147
|
-
if (content)
|
|
155
|
+
if (content) {
|
|
156
|
+
const msg = { role: "Meta AI", content };
|
|
157
|
+
if (timestamp) msg.timestamp = timestamp;
|
|
158
|
+
messages.push(msg);
|
|
159
|
+
}
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
162
|
return messages;
|
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared per-message timestamp normalization (decant-core).
|
|
3
|
+
*
|
|
4
|
+
* Parsers attach `timestamp` as ISO strings, locale strings, or numeric
|
|
5
|
+
* epochs (seconds or milliseconds). Display layers (ace) format whatever
|
|
6
|
+
* string they receive, so this helper keeps ISO/locale strings as-is and
|
|
7
|
+
* converts numeric epochs to ISO.
|
|
8
|
+
*
|
|
9
|
+
* Turn-granularity sources (Perplexity entries, Meta edges, DeepSeek pairs)
|
|
10
|
+
* reuse the same timestamp for both prompt and response — callers pass the
|
|
11
|
+
* same value to both messages.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Epoch values with abs < 1e11 are treated as seconds, else milliseconds. */
|
|
15
|
+
export function normalizeEpochToMs(value) {
|
|
16
|
+
if (Math.abs(value) < 1e11) return value * 1000;
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalizes a raw timestamp to a display-ready string.
|
|
22
|
+
* @param {unknown} value ISO string, locale string, epoch number, or numeric string.
|
|
23
|
+
* @returns {string|null} ISO string for epochs, trimmed input for date strings, else null.
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeTimestamp(value) {
|
|
26
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
27
|
+
try {
|
|
28
|
+
return new Date(normalizeEpochToMs(value)).toISOString();
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
if (!trimmed) return null;
|
|
36
|
+
if (/^[+-]?\d+(\.\d+)?$/.test(trimmed)) {
|
|
37
|
+
const numeric = Number(trimmed);
|
|
38
|
+
if (Number.isFinite(numeric)) {
|
|
39
|
+
try {
|
|
40
|
+
return new Date(normalizeEpochToMs(numeric)).toISOString();
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return trimmed;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Picks the first available timestamp from candidate fields.
|
|
54
|
+
* @param {object} source Parser payload object.
|
|
55
|
+
* @param {string[]} keys Field names to try in order.
|
|
56
|
+
* @returns {string|null}
|
|
57
|
+
*/
|
|
58
|
+
export function pickTimestamp(source, keys) {
|
|
59
|
+
if (!source || !Array.isArray(keys)) return null;
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
const normalized = normalizeTimestamp(source[key]);
|
|
62
|
+
if (normalized) return normalized;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|