mem-os-python 1.6.27__py3-none-any.whl
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.
- mem_os_python-1.6.27.dist-info/METADATA +1032 -0
- mem_os_python-1.6.27.dist-info/RECORD +253 -0
- mem_os_python-1.6.27.dist-info/WHEEL +4 -0
- mem_os_python-1.6.27.dist-info/entry_points.txt +2 -0
- mem_os_python-1.6.27.dist-info/licenses/LICENSE +21 -0
- memos/__init__.py +16 -0
- memos/_js/adapters/anthropic.d.ts +279 -0
- memos/_js/adapters/anthropic.d.ts.map +1 -0
- memos/_js/adapters/anthropic.js +348 -0
- memos/_js/adapters/anthropic.js.map +1 -0
- memos/_js/adapters/base.d.ts +10 -0
- memos/_js/adapters/base.d.ts.map +1 -0
- memos/_js/adapters/base.js +2 -0
- memos/_js/adapters/base.js.map +1 -0
- memos/_js/adapters/openai.d.ts +264 -0
- memos/_js/adapters/openai.d.ts.map +1 -0
- memos/_js/adapters/openai.js +342 -0
- memos/_js/adapters/openai.js.map +1 -0
- memos/_js/apps/explorer.d.ts +244 -0
- memos/_js/apps/explorer.d.ts.map +1 -0
- memos/_js/apps/explorer.js +738 -0
- memos/_js/apps/explorer.js.map +1 -0
- memos/_js/citations.d.ts +65 -0
- memos/_js/citations.d.ts.map +1 -0
- memos/_js/citations.js +143 -0
- memos/_js/citations.js.map +1 -0
- memos/_js/cli-browse.d.ts +3 -0
- memos/_js/cli-browse.d.ts.map +1 -0
- memos/_js/cli-browse.js +129 -0
- memos/_js/cli-browse.js.map +1 -0
- memos/_js/cli-completion.d.ts +31 -0
- memos/_js/cli-completion.d.ts.map +1 -0
- memos/_js/cli-completion.js +340 -0
- memos/_js/cli-completion.js.map +1 -0
- memos/_js/cli-help.d.ts +18 -0
- memos/_js/cli-help.d.ts.map +1 -0
- memos/_js/cli-help.js +357 -0
- memos/_js/cli-help.js.map +1 -0
- memos/_js/cli.d.ts +28 -0
- memos/_js/cli.d.ts.map +1 -0
- memos/_js/cli.js +2880 -0
- memos/_js/cli.js.map +1 -0
- memos/_js/compression.d.ts +25 -0
- memos/_js/compression.d.ts.map +1 -0
- memos/_js/compression.js +319 -0
- memos/_js/compression.js.map +1 -0
- memos/_js/confidence-machine.d.ts +99 -0
- memos/_js/confidence-machine.d.ts.map +1 -0
- memos/_js/confidence-machine.js +222 -0
- memos/_js/confidence-machine.js.map +1 -0
- memos/_js/context-pack.d.ts +506 -0
- memos/_js/context-pack.d.ts.map +1 -0
- memos/_js/context-pack.js +1435 -0
- memos/_js/context-pack.js.map +1 -0
- memos/_js/contradictions.d.ts +160 -0
- memos/_js/contradictions.d.ts.map +1 -0
- memos/_js/contradictions.js +255 -0
- memos/_js/contradictions.js.map +1 -0
- memos/_js/dashboard.d.ts +30 -0
- memos/_js/dashboard.d.ts.map +1 -0
- memos/_js/dashboard.js +302 -0
- memos/_js/dashboard.js.map +1 -0
- memos/_js/ebbinghaus.d.ts +29 -0
- memos/_js/ebbinghaus.d.ts.map +1 -0
- memos/_js/ebbinghaus.js +32 -0
- memos/_js/ebbinghaus.js.map +1 -0
- memos/_js/embedding-queue.d.ts +190 -0
- memos/_js/embedding-queue.d.ts.map +1 -0
- memos/_js/embedding-queue.js +316 -0
- memos/_js/embedding-queue.js.map +1 -0
- memos/_js/embeddings.d.ts +187 -0
- memos/_js/embeddings.d.ts.map +1 -0
- memos/_js/embeddings.js +633 -0
- memos/_js/embeddings.js.map +1 -0
- memos/_js/entity-extraction.d.ts +54 -0
- memos/_js/entity-extraction.d.ts.map +1 -0
- memos/_js/entity-extraction.js +325 -0
- memos/_js/entity-extraction.js.map +1 -0
- memos/_js/entity-index.d.ts +93 -0
- memos/_js/entity-index.d.ts.map +1 -0
- memos/_js/entity-index.js +165 -0
- memos/_js/entity-index.js.map +1 -0
- memos/_js/event-memory.d.ts +147 -0
- memos/_js/event-memory.d.ts.map +1 -0
- memos/_js/event-memory.js +290 -0
- memos/_js/event-memory.js.map +1 -0
- memos/_js/external-import.d.ts +98 -0
- memos/_js/external-import.d.ts.map +1 -0
- memos/_js/external-import.js +669 -0
- memos/_js/external-import.js.map +1 -0
- memos/_js/fidelity.d.ts +367 -0
- memos/_js/fidelity.d.ts.map +1 -0
- memos/_js/fidelity.js +911 -0
- memos/_js/fidelity.js.map +1 -0
- memos/_js/graph-expansion.d.ts +119 -0
- memos/_js/graph-expansion.d.ts.map +1 -0
- memos/_js/graph-expansion.js +213 -0
- memos/_js/graph-expansion.js.map +1 -0
- memos/_js/graph-mermaid.d.ts +13 -0
- memos/_js/graph-mermaid.d.ts.map +1 -0
- memos/_js/graph-mermaid.js +32 -0
- memos/_js/graph-mermaid.js.map +1 -0
- memos/_js/graph.d.ts +170 -0
- memos/_js/graph.d.ts.map +1 -0
- memos/_js/graph.js +482 -0
- memos/_js/graph.js.map +1 -0
- memos/_js/harness.d.ts +63 -0
- memos/_js/harness.d.ts.map +1 -0
- memos/_js/harness.js +93 -0
- memos/_js/harness.js.map +1 -0
- memos/_js/importance.d.ts +118 -0
- memos/_js/importance.d.ts.map +1 -0
- memos/_js/importance.js +144 -0
- memos/_js/importance.js.map +1 -0
- memos/_js/index.d.ts +51 -0
- memos/_js/index.d.ts.map +1 -0
- memos/_js/index.js +53 -0
- memos/_js/index.js.map +1 -0
- memos/_js/mcp.d.ts +58 -0
- memos/_js/mcp.d.ts.map +1 -0
- memos/_js/mcp.js +1853 -0
- memos/_js/mcp.js.map +1 -0
- memos/_js/memory.d.ts +1762 -0
- memos/_js/memory.d.ts.map +1 -0
- memos/_js/memory.js +6283 -0
- memos/_js/memory.js.map +1 -0
- memos/_js/procedural.d.ts +74 -0
- memos/_js/procedural.d.ts.map +1 -0
- memos/_js/procedural.js +125 -0
- memos/_js/procedural.js.map +1 -0
- memos/_js/project-config.d.ts +68 -0
- memos/_js/project-config.d.ts.map +1 -0
- memos/_js/project-config.js +59 -0
- memos/_js/project-config.js.map +1 -0
- memos/_js/proof-of-forgetting.d.ts +57 -0
- memos/_js/proof-of-forgetting.d.ts.map +1 -0
- memos/_js/proof-of-forgetting.js +80 -0
- memos/_js/proof-of-forgetting.js.map +1 -0
- memos/_js/provenance.d.ts +122 -0
- memos/_js/provenance.d.ts.map +1 -0
- memos/_js/provenance.js +182 -0
- memos/_js/provenance.js.map +1 -0
- memos/_js/quarantine-commands.d.ts +23 -0
- memos/_js/quarantine-commands.d.ts.map +1 -0
- memos/_js/quarantine-commands.js +42 -0
- memos/_js/quarantine-commands.js.map +1 -0
- memos/_js/quarantine.d.ts +120 -0
- memos/_js/quarantine.d.ts.map +1 -0
- memos/_js/quarantine.js +873 -0
- memos/_js/quarantine.js.map +1 -0
- memos/_js/redact.d.ts +12 -0
- memos/_js/redact.d.ts.map +1 -0
- memos/_js/redact.js +26 -0
- memos/_js/redact.js.map +1 -0
- memos/_js/repl.d.ts +57 -0
- memos/_js/repl.d.ts.map +1 -0
- memos/_js/repl.js +295 -0
- memos/_js/repl.js.map +1 -0
- memos/_js/rerank-local.d.ts +61 -0
- memos/_js/rerank-local.d.ts.map +1 -0
- memos/_js/rerank-local.js +210 -0
- memos/_js/rerank-local.js.map +1 -0
- memos/_js/retain-filter.d.ts +98 -0
- memos/_js/retain-filter.d.ts.map +1 -0
- memos/_js/retain-filter.js +232 -0
- memos/_js/retain-filter.js.map +1 -0
- memos/_js/retrieval.d.ts +265 -0
- memos/_js/retrieval.d.ts.map +1 -0
- memos/_js/retrieval.js +1018 -0
- memos/_js/retrieval.js.map +1 -0
- memos/_js/revert.d.ts +77 -0
- memos/_js/revert.d.ts.map +1 -0
- memos/_js/revert.js +252 -0
- memos/_js/revert.js.map +1 -0
- memos/_js/scope.d.ts +30 -0
- memos/_js/scope.d.ts.map +1 -0
- memos/_js/scope.js +71 -0
- memos/_js/scope.js.map +1 -0
- memos/_js/semantic-screen.d.ts +152 -0
- memos/_js/semantic-screen.d.ts.map +1 -0
- memos/_js/semantic-screen.js +290 -0
- memos/_js/semantic-screen.js.map +1 -0
- memos/_js/session-hooks.d.ts +127 -0
- memos/_js/session-hooks.d.ts.map +1 -0
- memos/_js/session-hooks.js +482 -0
- memos/_js/session-hooks.js.map +1 -0
- memos/_js/storage/sqlite.d.ts +427 -0
- memos/_js/storage/sqlite.d.ts.map +1 -0
- memos/_js/storage/sqlite.js +2428 -0
- memos/_js/storage/sqlite.js.map +1 -0
- memos/_js/sync.d.ts +213 -0
- memos/_js/sync.d.ts.map +1 -0
- memos/_js/sync.js +389 -0
- memos/_js/sync.js.map +1 -0
- memos/_js/tamper-log.d.ts +98 -0
- memos/_js/tamper-log.d.ts.map +1 -0
- memos/_js/tamper-log.js +327 -0
- memos/_js/tamper-log.js.map +1 -0
- memos/_js/temporal.d.ts +92 -0
- memos/_js/temporal.d.ts.map +1 -0
- memos/_js/temporal.js +536 -0
- memos/_js/temporal.js.map +1 -0
- memos/_js/time-travel.d.ts +38 -0
- memos/_js/time-travel.d.ts.map +1 -0
- memos/_js/time-travel.js +46 -0
- memos/_js/time-travel.js.map +1 -0
- memos/_js/timestamp-format.d.ts +18 -0
- memos/_js/timestamp-format.d.ts.map +1 -0
- memos/_js/timestamp-format.js +52 -0
- memos/_js/timestamp-format.js.map +1 -0
- memos/_js/triple-extraction.d.ts +31 -0
- memos/_js/triple-extraction.d.ts.map +1 -0
- memos/_js/triple-extraction.js +116 -0
- memos/_js/triple-extraction.js.map +1 -0
- memos/_js/types.d.ts +2474 -0
- memos/_js/types.d.ts.map +1 -0
- memos/_js/types.js +20 -0
- memos/_js/types.js.map +1 -0
- memos/_js/updater.d.ts +95 -0
- memos/_js/updater.d.ts.map +1 -0
- memos/_js/updater.js +268 -0
- memos/_js/updater.js.map +1 -0
- memos/_js/version.d.ts +17 -0
- memos/_js/version.d.ts.map +1 -0
- memos/_js/version.js +34 -0
- memos/_js/version.js.map +1 -0
- memos/_js/vision/clip-query-cache.d.ts +58 -0
- memos/_js/vision/clip-query-cache.d.ts.map +1 -0
- memos/_js/vision/clip-query-cache.js +79 -0
- memos/_js/vision/clip-query-cache.js.map +1 -0
- memos/_js/vision/clip.d.ts +113 -0
- memos/_js/vision/clip.d.ts.map +1 -0
- memos/_js/vision/clip.js +224 -0
- memos/_js/vision/clip.js.map +1 -0
- memos/_js/vision/fake-clip.d.ts +55 -0
- memos/_js/vision/fake-clip.d.ts.map +1 -0
- memos/_js/vision/fake-clip.js +122 -0
- memos/_js/vision/fake-clip.js.map +1 -0
- memos/_js/vision/index.d.ts +15 -0
- memos/_js/vision/index.d.ts.map +1 -0
- memos/_js/vision/index.js +13 -0
- memos/_js/vision/index.js.map +1 -0
- memos/_js/vision/store.d.ts +137 -0
- memos/_js/vision/store.d.ts.map +1 -0
- memos/_js/vision/store.js +383 -0
- memos/_js/vision/store.js.map +1 -0
- memos/adapters/__init__.py +0 -0
- memos/adapters/crewai.py +385 -0
- memos/adapters/langchain.py +301 -0
- memos/adapters/ollama.py +396 -0
- memos/server/__init__.py +0 -0
- memos/server/main.py +547 -0
- memos/server/routes.py +360 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for third-party AI memory exports — ChatGPT data exports and
|
|
3
|
+
* Claude data exports — feeding `MemOS.importExternal`.
|
|
4
|
+
*
|
|
5
|
+
* The design goal is tolerance: vendor export shapes change without
|
|
6
|
+
* notice, so the parser sniffs each item rather than trusting a fixed
|
|
7
|
+
* schema, extracts whatever text fields exist, and reports what it
|
|
8
|
+
* skipped. Recognized shapes:
|
|
9
|
+
*
|
|
10
|
+
* - ChatGPT data export (`conversations.json`): an array of
|
|
11
|
+
* `{ title, mapping: { <id>: { message: { author: { role }, content:
|
|
12
|
+
* { parts: string[] } } } } }` trees — user turns are extracted.
|
|
13
|
+
* - Claude data export (`conversations.json`): an array of
|
|
14
|
+
* `{ name, chat_messages: [{ sender: "human", text, created_at }] }` —
|
|
15
|
+
* human turns are extracted. A directory export is scanned for every
|
|
16
|
+
* `*.json` / `*.jsonl` file and each candidate is sniffed.
|
|
17
|
+
* - Slack export (directory): `channels.json` + `users.json` plus one
|
|
18
|
+
* folder per channel with per-day message files
|
|
19
|
+
* (`{ user, text, ts }`) — all messages are extracted with author and
|
|
20
|
+
* channel attribution.
|
|
21
|
+
* - Flat memory lists: an array of strings or of objects with
|
|
22
|
+
* `content` / `text` / `title` / `memory` string fields (covers
|
|
23
|
+
* `memories.json`-style exports from both vendors).
|
|
24
|
+
* - Wrapper objects: `{ conversations: [...] }` or `{ memories: [...] }`.
|
|
25
|
+
*
|
|
26
|
+
* Dedupe is by SHA-256 of the normalized content (whitespace-collapsed,
|
|
27
|
+
* lowercased), so the same message appearing in several files — or twice
|
|
28
|
+
* in one export — is imported once.
|
|
29
|
+
*
|
|
30
|
+
* @module @mem-os/external-import
|
|
31
|
+
*/
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
33
|
+
const MIN_CONTENT_LENGTH = 3;
|
|
34
|
+
const DEFAULT_MAX_ITEMS = 500;
|
|
35
|
+
function asRecord(value) {
|
|
36
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
37
|
+
? value
|
|
38
|
+
: null;
|
|
39
|
+
}
|
|
40
|
+
function firstString(record, keys) {
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
const value = record[key];
|
|
43
|
+
if (typeof value === "string" &&
|
|
44
|
+
value.trim().length >= MIN_CONTENT_LENGTH) {
|
|
45
|
+
return value.trim();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Extract user-turn texts from a ChatGPT conversation `mapping` tree. */
|
|
51
|
+
function parseChatGptMapping(mapping, conversation) {
|
|
52
|
+
const items = [];
|
|
53
|
+
if (!mapping || typeof mapping !== "object")
|
|
54
|
+
return items;
|
|
55
|
+
for (const node of Object.values(mapping)) {
|
|
56
|
+
const record = asRecord(node);
|
|
57
|
+
const message = record ? asRecord(record.message) : null;
|
|
58
|
+
if (!message)
|
|
59
|
+
continue;
|
|
60
|
+
const author = asRecord(message.author);
|
|
61
|
+
const role = author ? String(author.role ?? "") : "";
|
|
62
|
+
if (role !== "user")
|
|
63
|
+
continue;
|
|
64
|
+
const content = asRecord(message.content);
|
|
65
|
+
const parts = content?.parts;
|
|
66
|
+
if (!Array.isArray(parts))
|
|
67
|
+
continue;
|
|
68
|
+
const text = parts
|
|
69
|
+
.filter((p) => typeof p === "string" && p.trim().length >= MIN_CONTENT_LENGTH)
|
|
70
|
+
.join("\n")
|
|
71
|
+
.trim();
|
|
72
|
+
if (!text)
|
|
73
|
+
continue;
|
|
74
|
+
const createTime = typeof message.create_time === "number"
|
|
75
|
+
? message.create_time * 1000
|
|
76
|
+
: undefined;
|
|
77
|
+
items.push({
|
|
78
|
+
content: text,
|
|
79
|
+
...(createTime ? { createdAt: createTime } : {}),
|
|
80
|
+
...(conversation ? { conversation } : {}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return items;
|
|
84
|
+
}
|
|
85
|
+
/** Extract human-turn texts from a Claude conversation export entry. */
|
|
86
|
+
function parseClaudeChatMessages(messages, conversation) {
|
|
87
|
+
const items = [];
|
|
88
|
+
if (!Array.isArray(messages))
|
|
89
|
+
return items;
|
|
90
|
+
for (const message of messages) {
|
|
91
|
+
const record = asRecord(message);
|
|
92
|
+
if (!record)
|
|
93
|
+
continue;
|
|
94
|
+
const sender = String(record.sender ?? "");
|
|
95
|
+
if (sender !== "human")
|
|
96
|
+
continue;
|
|
97
|
+
const text = typeof record.text === "string" ? record.text.trim() : "";
|
|
98
|
+
if (text.length < MIN_CONTENT_LENGTH)
|
|
99
|
+
continue;
|
|
100
|
+
let createdAt;
|
|
101
|
+
if (typeof record.created_at === "string") {
|
|
102
|
+
const parsed = Date.parse(record.created_at);
|
|
103
|
+
createdAt = Number.isNaN(parsed) ? undefined : parsed;
|
|
104
|
+
}
|
|
105
|
+
items.push({
|
|
106
|
+
content: text,
|
|
107
|
+
...(createdAt ? { createdAt } : {}),
|
|
108
|
+
...(conversation ? { conversation } : {}),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return items;
|
|
112
|
+
}
|
|
113
|
+
/** Flat memory entry: string or an object with a text-ish field. */
|
|
114
|
+
function parseGenericEntry(entry) {
|
|
115
|
+
if (typeof entry === "string") {
|
|
116
|
+
const text = entry.trim();
|
|
117
|
+
return text.length >= MIN_CONTENT_LENGTH ? { content: text } : null;
|
|
118
|
+
}
|
|
119
|
+
const record = asRecord(entry);
|
|
120
|
+
if (!record)
|
|
121
|
+
return null;
|
|
122
|
+
const text = firstString(record, [
|
|
123
|
+
"content",
|
|
124
|
+
"text",
|
|
125
|
+
"title",
|
|
126
|
+
"memory",
|
|
127
|
+
"value",
|
|
128
|
+
]);
|
|
129
|
+
if (!text)
|
|
130
|
+
return null;
|
|
131
|
+
let createdAt;
|
|
132
|
+
for (const key of ["created_at", "createdAt", "create_time"]) {
|
|
133
|
+
const value = record[key];
|
|
134
|
+
if (typeof value === "number" && value > 0) {
|
|
135
|
+
// Heuristic: seconds vs milliseconds epochs.
|
|
136
|
+
createdAt = value < 10_000_000_000 ? value * 1000 : value;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (typeof value === "string") {
|
|
140
|
+
const parsed = Date.parse(value);
|
|
141
|
+
if (!Number.isNaN(parsed)) {
|
|
142
|
+
createdAt = parsed;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { content: text, ...(createdAt ? { createdAt } : {}) };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Parse a parsed-JSON export payload into memory items.
|
|
151
|
+
*
|
|
152
|
+
* @param raw — The decoded JSON value (or a JSON string).
|
|
153
|
+
* @param source — `"auto"` sniffs the shape; explicit values force a
|
|
154
|
+
* parser (useful when auto-detection misreads a custom format).
|
|
155
|
+
*/
|
|
156
|
+
export function parseExternalMemoryExport(raw, source = "auto", maxItems = DEFAULT_MAX_ITEMS) {
|
|
157
|
+
let payload = raw;
|
|
158
|
+
if (typeof payload === "string") {
|
|
159
|
+
try {
|
|
160
|
+
payload = JSON.parse(payload);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { items: [], detected: "generic", skipped: 0 };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let root = payload;
|
|
167
|
+
let detected = source === "auto" ? "generic" : source;
|
|
168
|
+
// Unwrap container objects.
|
|
169
|
+
if (!Array.isArray(root) && asRecord(root)) {
|
|
170
|
+
const record = root;
|
|
171
|
+
if (Array.isArray(record.conversations)) {
|
|
172
|
+
root = record.conversations;
|
|
173
|
+
}
|
|
174
|
+
else if (Array.isArray(record.memories)) {
|
|
175
|
+
root = record.memories;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const items = [];
|
|
179
|
+
let skipped = 0;
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
const push = (item) => {
|
|
182
|
+
if (!item) {
|
|
183
|
+
skipped += 1;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Content-hash dedupe: the same message appearing twice in an export
|
|
187
|
+
// (or across files in a directory scan) is imported once.
|
|
188
|
+
const key = contentHash(item.content);
|
|
189
|
+
if (seen.has(key)) {
|
|
190
|
+
skipped += 1;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (items.length >= maxItems) {
|
|
194
|
+
skipped += 1;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
seen.add(key);
|
|
198
|
+
items.push(item);
|
|
199
|
+
};
|
|
200
|
+
if (Array.isArray(root)) {
|
|
201
|
+
for (const entry of root) {
|
|
202
|
+
const record = asRecord(entry);
|
|
203
|
+
const isChatGpt = source === "chatgpt" ||
|
|
204
|
+
(source === "auto" &&
|
|
205
|
+
record &&
|
|
206
|
+
"mapping" in record &&
|
|
207
|
+
asRecord(record.mapping));
|
|
208
|
+
const isClaude = source === "claude" ||
|
|
209
|
+
(source === "auto" && record && "chat_messages" in record);
|
|
210
|
+
if (isChatGpt && record) {
|
|
211
|
+
detected = "chatgpt";
|
|
212
|
+
const conversation = typeof record.title === "string" ? record.title : undefined;
|
|
213
|
+
const turns = parseChatGptMapping(record.mapping, conversation);
|
|
214
|
+
if (turns.length === 0)
|
|
215
|
+
skipped += 1;
|
|
216
|
+
for (const turn of turns)
|
|
217
|
+
push(turn);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (isClaude && record) {
|
|
221
|
+
detected = "claude";
|
|
222
|
+
const conversation = typeof record.name === "string" ? record.name : undefined;
|
|
223
|
+
const turns = parseClaudeChatMessages(record.chat_messages, conversation);
|
|
224
|
+
if (turns.length === 0)
|
|
225
|
+
skipped += 1;
|
|
226
|
+
for (const turn of turns)
|
|
227
|
+
push(turn);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
push(parseGenericEntry(entry));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
skipped += 1;
|
|
235
|
+
}
|
|
236
|
+
return { items, detected, skipped };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* SHA-256 of the normalized content (whitespace-collapsed, lowercased).
|
|
240
|
+
* The dedupe key for imports.
|
|
241
|
+
*/
|
|
242
|
+
export function contentHash(content) {
|
|
243
|
+
const normalized = content.replace(/\s+/g, " ").trim().toLowerCase();
|
|
244
|
+
return createHash("sha256").update(normalized, "utf8").digest("hex");
|
|
245
|
+
}
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Export-directory scanning
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
const MAX_SCAN_FILES = 500;
|
|
250
|
+
const MAX_SCAN_FILE_BYTES = 10 * 1024 * 1024;
|
|
251
|
+
/**
|
|
252
|
+
* Scan an export directory for a vendor source and parse everything
|
|
253
|
+
* found, with content-hash dedupe across files.
|
|
254
|
+
*
|
|
255
|
+
* - `slack`: reads the official Slack export layout (`channels.json`,
|
|
256
|
+
* `users.json`, per-channel per-day message files).
|
|
257
|
+
* - `chatgpt` / `claude`: walks the directory for `*.json` files
|
|
258
|
+
* (each sniffed as a conversation export) and `*.jsonl` files
|
|
259
|
+
* (each line sniffed as a message).
|
|
260
|
+
*/
|
|
261
|
+
export async function parseExportDirectory(dir, source, maxItems = DEFAULT_MAX_ITEMS) {
|
|
262
|
+
if (source === "slack")
|
|
263
|
+
return parseSlackExportDirectory(dir, maxItems);
|
|
264
|
+
const { readdir, readFile, stat } = await import("node:fs/promises");
|
|
265
|
+
const { join, extname } = await import("node:path");
|
|
266
|
+
const files = [];
|
|
267
|
+
const walk = async (current, depth) => {
|
|
268
|
+
if (depth > 4 || files.length >= MAX_SCAN_FILES)
|
|
269
|
+
return;
|
|
270
|
+
let entries;
|
|
271
|
+
try {
|
|
272
|
+
entries = await readdir(current);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
for (const entry of entries) {
|
|
278
|
+
const full = join(current, entry);
|
|
279
|
+
let st;
|
|
280
|
+
try {
|
|
281
|
+
st = await stat(full);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (st.isDirectory()) {
|
|
287
|
+
await walk(full, depth + 1);
|
|
288
|
+
}
|
|
289
|
+
else if (st.isFile() &&
|
|
290
|
+
st.size <= MAX_SCAN_FILE_BYTES &&
|
|
291
|
+
(extname(entry) === ".json" || extname(entry) === ".jsonl") &&
|
|
292
|
+
files.length < MAX_SCAN_FILES) {
|
|
293
|
+
files.push(full);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
try {
|
|
298
|
+
const rootStat = await stat(dir);
|
|
299
|
+
if (!rootStat.isDirectory()) {
|
|
300
|
+
return { items: [], detected: source, skipped: 1 };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return { items: [], detected: source, skipped: 1 };
|
|
305
|
+
}
|
|
306
|
+
await walk(dir, 0);
|
|
307
|
+
const items = [];
|
|
308
|
+
let skipped = 0;
|
|
309
|
+
const seen = new Set();
|
|
310
|
+
const push = (item) => {
|
|
311
|
+
if (!item) {
|
|
312
|
+
skipped += 1;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const key = contentHash(item.content);
|
|
316
|
+
if (seen.has(key)) {
|
|
317
|
+
skipped += 1;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (items.length >= maxItems) {
|
|
321
|
+
skipped += 1;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
seen.add(key);
|
|
325
|
+
items.push(item);
|
|
326
|
+
};
|
|
327
|
+
for (const file of files) {
|
|
328
|
+
let raw;
|
|
329
|
+
try {
|
|
330
|
+
raw = await readFile(file, "utf8");
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
skipped += 1;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (extname(file) === ".jsonl") {
|
|
337
|
+
for (const line of raw.split("\n")) {
|
|
338
|
+
if (!line.trim())
|
|
339
|
+
continue;
|
|
340
|
+
push(parseJsonlLine(line, source));
|
|
341
|
+
}
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
let payload;
|
|
345
|
+
try {
|
|
346
|
+
payload = JSON.parse(raw);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
skipped += 1;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const parsed = parseExternalMemoryExport(payload, source, maxItems);
|
|
353
|
+
for (const item of parsed.items)
|
|
354
|
+
push(item);
|
|
355
|
+
skipped += parsed.skipped;
|
|
356
|
+
}
|
|
357
|
+
return { items, detected: source, skipped };
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Sniff one JSONL line as a message. Tolerates Claude Code session
|
|
361
|
+
* history (`{ type: "human", message: { content } }`), simple
|
|
362
|
+
* `{ role, content }` / `{ sender, text }` shapes, and falls back to
|
|
363
|
+
* the generic entry parser.
|
|
364
|
+
*/
|
|
365
|
+
function parseJsonlLine(line, source) {
|
|
366
|
+
let value;
|
|
367
|
+
try {
|
|
368
|
+
value = JSON.parse(line);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const record = asRecord(value);
|
|
374
|
+
if (!record)
|
|
375
|
+
return parseGenericEntry(value);
|
|
376
|
+
// Claude Code session history: { type: "human"|"assistant", message: {...} }
|
|
377
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
378
|
+
const message = asRecord(record.message);
|
|
379
|
+
if (message && (type === "human" || type === "user")) {
|
|
380
|
+
const text = messageText(message.content);
|
|
381
|
+
if (text)
|
|
382
|
+
return { content: text, conversation: source };
|
|
383
|
+
}
|
|
384
|
+
// { role: "user", content } shape
|
|
385
|
+
const role = typeof record.role === "string" ? record.role : "";
|
|
386
|
+
if (role === "user" || role === "human") {
|
|
387
|
+
const text = messageText(record.content);
|
|
388
|
+
if (text)
|
|
389
|
+
return { content: text, conversation: source };
|
|
390
|
+
}
|
|
391
|
+
// { sender: "human", text } shape (Claude export per-message)
|
|
392
|
+
if (String(record.sender ?? "") === "human") {
|
|
393
|
+
const text = typeof record.text === "string" ? record.text.trim() : "";
|
|
394
|
+
if (text.length >= MIN_CONTENT_LENGTH) {
|
|
395
|
+
return { content: text, conversation: source };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return parseGenericEntry(value);
|
|
399
|
+
}
|
|
400
|
+
/** Pull plain text out of a string or Anthropic-style content blocks. */
|
|
401
|
+
function messageText(content) {
|
|
402
|
+
if (typeof content === "string") {
|
|
403
|
+
const text = content.trim();
|
|
404
|
+
return text.length >= MIN_CONTENT_LENGTH ? text : null;
|
|
405
|
+
}
|
|
406
|
+
if (Array.isArray(content)) {
|
|
407
|
+
const text = content
|
|
408
|
+
.map((block) => {
|
|
409
|
+
const record = asRecord(block);
|
|
410
|
+
const t = record?.text;
|
|
411
|
+
return typeof t === "string" ? t : "";
|
|
412
|
+
})
|
|
413
|
+
.join("\n")
|
|
414
|
+
.trim();
|
|
415
|
+
return text.length >= MIN_CONTENT_LENGTH ? text : null;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Parse an official Slack export directory:
|
|
421
|
+
* `channels.json` + `users.json` at the root, one folder per channel
|
|
422
|
+
* with per-day message files (`[{ user, text, ts }]`).
|
|
423
|
+
*/
|
|
424
|
+
export async function parseSlackExportDirectory(dir, maxItems = DEFAULT_MAX_ITEMS) {
|
|
425
|
+
const { readFile, readdir, stat } = await import("node:fs/promises");
|
|
426
|
+
const { join, extname } = await import("node:path");
|
|
427
|
+
const readJson = async (path) => {
|
|
428
|
+
try {
|
|
429
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
try {
|
|
436
|
+
const st = await stat(dir);
|
|
437
|
+
if (!st.isDirectory()) {
|
|
438
|
+
return { items: [], detected: "slack", skipped: 1 };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return { items: [], detected: "slack", skipped: 1 };
|
|
443
|
+
}
|
|
444
|
+
// Channel id → name and user id → display name lookups.
|
|
445
|
+
const channelNames = new Map();
|
|
446
|
+
const channels = await readJson(join(dir, "channels.json"));
|
|
447
|
+
if (Array.isArray(channels)) {
|
|
448
|
+
for (const c of channels) {
|
|
449
|
+
const record = asRecord(c);
|
|
450
|
+
if (record && typeof record.id === "string") {
|
|
451
|
+
const name = typeof record.name === "string" ? record.name : record.id;
|
|
452
|
+
channelNames.set(record.id, name);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const userNames = new Map();
|
|
457
|
+
const users = await readJson(join(dir, "users.json"));
|
|
458
|
+
if (Array.isArray(users)) {
|
|
459
|
+
for (const u of users) {
|
|
460
|
+
const record = asRecord(u);
|
|
461
|
+
if (record && typeof record.id === "string") {
|
|
462
|
+
const profile = asRecord(record.profile);
|
|
463
|
+
const name = (typeof profile?.display_name === "string" && profile.display_name) ||
|
|
464
|
+
(typeof profile?.real_name === "string" && profile.real_name) ||
|
|
465
|
+
(typeof record.real_name === "string" && record.real_name) ||
|
|
466
|
+
(typeof record.name === "string" && record.name) ||
|
|
467
|
+
record.id;
|
|
468
|
+
userNames.set(record.id, name);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Collect per-day message files; the channel label is the parent
|
|
473
|
+
// folder name, resolved through channels.json when it is an id.
|
|
474
|
+
const dayFiles = [];
|
|
475
|
+
let rootEntries;
|
|
476
|
+
try {
|
|
477
|
+
rootEntries = await readdir(dir);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return { items: [], detected: "slack", skipped: 1 };
|
|
481
|
+
}
|
|
482
|
+
for (const entry of rootEntries) {
|
|
483
|
+
const full = join(dir, entry);
|
|
484
|
+
let st;
|
|
485
|
+
try {
|
|
486
|
+
st = await stat(full);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (!st.isDirectory())
|
|
492
|
+
continue;
|
|
493
|
+
const channel = channelNames.get(entry) ?? entry;
|
|
494
|
+
let dayEntries;
|
|
495
|
+
try {
|
|
496
|
+
dayEntries = await readdir(full);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
for (const day of dayEntries) {
|
|
502
|
+
if (extname(day) === ".json") {
|
|
503
|
+
dayFiles.push({ path: join(full, day), channel });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
dayFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
508
|
+
const items = [];
|
|
509
|
+
let skipped = 0;
|
|
510
|
+
const seen = new Set();
|
|
511
|
+
for (const { path, channel } of dayFiles) {
|
|
512
|
+
const payload = await readJson(path);
|
|
513
|
+
if (!Array.isArray(payload)) {
|
|
514
|
+
skipped += 1;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
for (const entry of payload) {
|
|
518
|
+
const record = asRecord(entry);
|
|
519
|
+
if (!record) {
|
|
520
|
+
skipped += 1;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const rawText = typeof record.text === "string" ? record.text : "";
|
|
524
|
+
const text = unescapeSlackText(rawText).trim();
|
|
525
|
+
if (text.length < MIN_CONTENT_LENGTH) {
|
|
526
|
+
skipped += 1;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const key = contentHash(text);
|
|
530
|
+
if (seen.has(key)) {
|
|
531
|
+
skipped += 1;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (items.length >= maxItems) {
|
|
535
|
+
skipped += 1;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
seen.add(key);
|
|
539
|
+
const userId = typeof record.user === "string" ? record.user : "";
|
|
540
|
+
const author = userId ? (userNames.get(userId) ?? userId) : undefined;
|
|
541
|
+
const ts = typeof record.ts === "string" ? record.ts : "";
|
|
542
|
+
const createdAt = slackTsToMs(ts);
|
|
543
|
+
items.push({
|
|
544
|
+
content: text,
|
|
545
|
+
...(createdAt ? { createdAt } : {}),
|
|
546
|
+
...(author ? { author } : {}),
|
|
547
|
+
channel,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Chronological order — Slack day files are already sorted by path,
|
|
552
|
+
// but multi-channel scans interleave them.
|
|
553
|
+
items.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
|
554
|
+
return { items, detected: "slack", skipped };
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Slack message text carries HTML entities (`&` etc.).
|
|
558
|
+
* `&` MUST be decoded last: decoding it first turns an escaped
|
|
559
|
+
* `&lt;` (i.e. literal "<" text) into `<`, which the next pass
|
|
560
|
+
* would decode a second time into `<` — a double-unescape.
|
|
561
|
+
*/
|
|
562
|
+
function unescapeSlackText(text) {
|
|
563
|
+
return text
|
|
564
|
+
.replace(/</g, "<")
|
|
565
|
+
.replace(/>/g, ">")
|
|
566
|
+
.replace(/"/g, '"')
|
|
567
|
+
.replace(/'/g, "'")
|
|
568
|
+
.replace(/&/g, "&");
|
|
569
|
+
}
|
|
570
|
+
/** Slack `ts` is `"1718452800.000200"` (seconds.fraction) → Unix ms. */
|
|
571
|
+
function slackTsToMs(ts) {
|
|
572
|
+
if (!ts)
|
|
573
|
+
return undefined;
|
|
574
|
+
const seconds = Number.parseFloat(ts);
|
|
575
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
576
|
+
return undefined;
|
|
577
|
+
return Math.floor(seconds * 1000);
|
|
578
|
+
}
|
|
579
|
+
const SYNTHESIS_RULES = [
|
|
580
|
+
{
|
|
581
|
+
kind: "decision",
|
|
582
|
+
patterns: [
|
|
583
|
+
/\bdecided to\b/i,
|
|
584
|
+
/\bdecide to\b/i,
|
|
585
|
+
/\bwe chose\b/i,
|
|
586
|
+
/\bchose to\b/i,
|
|
587
|
+
/\bgoing with\b/i,
|
|
588
|
+
/\bsettled on\b/i,
|
|
589
|
+
/\bagreed to\b/i,
|
|
590
|
+
/\bagreed on\b/i,
|
|
591
|
+
/\bopted for\b/i,
|
|
592
|
+
/\bdecision:/i,
|
|
593
|
+
],
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
kind: "preference",
|
|
597
|
+
patterns: [
|
|
598
|
+
/\bi prefer\b/i,
|
|
599
|
+
/\bmy favorite\b/i,
|
|
600
|
+
/\bmy favourite\b/i,
|
|
601
|
+
/\bi like\b/i,
|
|
602
|
+
/\bi love\b/i,
|
|
603
|
+
/\bi dislike\b/i,
|
|
604
|
+
/\bi hate\b/i,
|
|
605
|
+
/\bcan't stand\b/i,
|
|
606
|
+
/\bcannot stand\b/i,
|
|
607
|
+
/\bprefer .* over\b/i,
|
|
608
|
+
/\balways use\b/i,
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
kind: "milestone",
|
|
613
|
+
patterns: [
|
|
614
|
+
/\bshipped\b/i,
|
|
615
|
+
/\blaunched\b/i,
|
|
616
|
+
/\breleased\b/i,
|
|
617
|
+
/\bcompleted\b/i,
|
|
618
|
+
/\bfinished\b/i,
|
|
619
|
+
/\bdeployed\b/i,
|
|
620
|
+
/\bpublished\b/i,
|
|
621
|
+
/\bmerged\b/i,
|
|
622
|
+
/\bwent live\b/i,
|
|
623
|
+
],
|
|
624
|
+
},
|
|
625
|
+
];
|
|
626
|
+
const MIN_SYNTHESIS_SENTENCE = 12;
|
|
627
|
+
const MAX_SYNTHESIS_TEXT = 400;
|
|
628
|
+
/**
|
|
629
|
+
* Rule-based post-import pass: pull structured decisions, preferences,
|
|
630
|
+
* and milestones out of imported messages. Each match is the source
|
|
631
|
+
* *sentence* (not the whole message), classified by the first matching
|
|
632
|
+
* rule (decision > preference > milestone). No LLM — pure regex over
|
|
633
|
+
* sentence-split text.
|
|
634
|
+
*/
|
|
635
|
+
export function synthesizeImportInsights(items) {
|
|
636
|
+
const insights = [];
|
|
637
|
+
const seen = new Set();
|
|
638
|
+
for (const item of items) {
|
|
639
|
+
for (const sentence of splitSentences(item.content)) {
|
|
640
|
+
const text = sentence.trim();
|
|
641
|
+
if (text.length < MIN_SYNTHESIS_SENTENCE)
|
|
642
|
+
continue;
|
|
643
|
+
const kind = classifySynthesisSentence(text);
|
|
644
|
+
if (!kind)
|
|
645
|
+
continue;
|
|
646
|
+
const key = contentHash(text.slice(0, MAX_SYNTHESIS_TEXT));
|
|
647
|
+
if (seen.has(key))
|
|
648
|
+
continue;
|
|
649
|
+
seen.add(key);
|
|
650
|
+
insights.push({ kind, text: text.slice(0, MAX_SYNTHESIS_TEXT) });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return insights;
|
|
654
|
+
}
|
|
655
|
+
function splitSentences(text) {
|
|
656
|
+
return text
|
|
657
|
+
.replace(/\s+/g, " ")
|
|
658
|
+
.split(/(?<=[.!?])\s+/)
|
|
659
|
+
.map((s) => s.trim())
|
|
660
|
+
.filter((s) => s.length > 0);
|
|
661
|
+
}
|
|
662
|
+
function classifySynthesisSentence(sentence) {
|
|
663
|
+
for (const rule of SYNTHESIS_RULES) {
|
|
664
|
+
if (rule.patterns.some((re) => re.test(sentence)))
|
|
665
|
+
return rule.kind;
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
//# sourceMappingURL=external-import.js.map
|