@nguyentamdat/mempalace 1.0.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.
@@ -0,0 +1,6 @@
1
+ export {
2
+ detectRoomsFromFiles,
3
+ detectRoomsFromFolders,
4
+ detectRoomsLocal,
5
+ saveConfig,
6
+ } from "./room-detector-local";
@@ -0,0 +1,181 @@
1
+ import { basename } from "node:path";
2
+ import { ChromaClient, DefaultEmbeddingFunction } from "chromadb";
3
+ import { MempalaceConfig } from "./config";
4
+
5
+ type SearchOptions = {
6
+ query: string;
7
+ palacePath: string;
8
+ wing?: string;
9
+ room?: string;
10
+ nResults?: number;
11
+ };
12
+
13
+ type SearchHit = {
14
+ text: string;
15
+ wing: string;
16
+ room: string;
17
+ source_file: string;
18
+ similarity: number;
19
+ };
20
+
21
+ type DrawerCollection = Awaited<ReturnType<ChromaClient["getCollection"]>>;
22
+ type DrawerQueryParams = Parameters<DrawerCollection["query"]>[0];
23
+
24
+ function buildWhereFilter(wing?: string, room?: string) {
25
+ let where: Record<string, unknown> = {};
26
+
27
+ if (wing && room) {
28
+ where = { $and: [{ wing }, { room }] };
29
+ } else if (wing) {
30
+ where = { wing };
31
+ } else if (room) {
32
+ where = { room };
33
+ }
34
+
35
+ return where;
36
+ }
37
+
38
+ async function getDrawerCollection(palacePath: string) {
39
+ const config = new MempalaceConfig();
40
+ const collectionName = config.collectionName;
41
+ const client = new ChromaClient({ path: palacePath || config.palacePath });
42
+ const embeddingFunction = new DefaultEmbeddingFunction();
43
+
44
+ try {
45
+ return await client.getCollection({ name: collectionName, embeddingFunction });
46
+ } catch {
47
+ throw new Error(`No palace found at ${palacePath || config.palacePath}`);
48
+ }
49
+ }
50
+
51
+ export async function search({
52
+ query,
53
+ palacePath,
54
+ wing,
55
+ room,
56
+ nResults = 5,
57
+ }: SearchOptions): Promise<void> {
58
+ let col: DrawerCollection;
59
+
60
+ try {
61
+ col = await getDrawerCollection(palacePath);
62
+ } catch {
63
+ console.log(`\n No palace found at ${palacePath}`);
64
+ console.log(" Run: mempalace init <dir> then mempalace mine <dir>");
65
+ process.exit(1);
66
+ }
67
+
68
+ const where = buildWhereFilter(wing, room);
69
+
70
+ try {
71
+ const kwargs = {
72
+ query_texts: [query],
73
+ n_results: nResults,
74
+ include: ["documents", "metadatas", "distances"],
75
+ } as unknown as DrawerQueryParams;
76
+
77
+ if (Object.keys(where).length > 0) {
78
+ kwargs.where = where;
79
+ }
80
+
81
+ const results = await col.query(kwargs);
82
+ const docs = results.documents?.[0] ?? [];
83
+ const metas = results.metadatas?.[0] ?? [];
84
+ const dists = results.distances?.[0] ?? [];
85
+
86
+ if (docs.length === 0) {
87
+ console.log(`\n No results found for: "${query}"`);
88
+ return;
89
+ }
90
+
91
+ console.log(`\n${"=".repeat(60)}`);
92
+ console.log(` Results for: "${query}"`);
93
+ if (wing) console.log(` Wing: ${wing}`);
94
+ if (room) console.log(` Room: ${room}`);
95
+ console.log(`${"=".repeat(60)}\n`);
96
+
97
+ for (let i = 0; i < docs.length; i++) {
98
+ const doc = String(docs[i] ?? "");
99
+ const meta = (metas[i] ?? {}) as Record<string, unknown>;
100
+ const dist = Number(dists[i] ?? 0);
101
+ const similarity = Math.round((1 - dist) * 1000) / 1000;
102
+ const source = basename(String(meta.source_file ?? "?"));
103
+ const wingName = String(meta.wing ?? "?");
104
+ const roomName = String(meta.room ?? "?");
105
+
106
+ console.log(` [${i + 1}] ${wingName} / ${roomName}`);
107
+ console.log(` Source: ${source}`);
108
+ console.log(` Match: ${similarity}`);
109
+ console.log();
110
+
111
+ for (const line of doc.trim().split("\n")) {
112
+ console.log(` ${line}`);
113
+ }
114
+ console.log();
115
+ console.log(` ${"─".repeat(56)}`);
116
+ }
117
+
118
+ console.log();
119
+ } catch (error) {
120
+ console.log(`\n Search error: ${error}`);
121
+ process.exit(1);
122
+ }
123
+ }
124
+
125
+ export async function searchMemories({
126
+ query,
127
+ palacePath,
128
+ wing,
129
+ room,
130
+ nResults = 5,
131
+ }: SearchOptions): Promise<{ query?: string; filters?: { wing?: string; room?: string }; results?: SearchHit[]; error?: string }> {
132
+ let col: DrawerCollection;
133
+
134
+ try {
135
+ col = await getDrawerCollection(palacePath);
136
+ } catch (error) {
137
+ return { error: `No palace found at ${palacePath}: ${error}` };
138
+ }
139
+
140
+ const where = buildWhereFilter(wing, room);
141
+
142
+ try {
143
+ const kwargs = {
144
+ query_texts: [query],
145
+ n_results: nResults,
146
+ include: ["documents", "metadatas", "distances"],
147
+ } as unknown as DrawerQueryParams;
148
+
149
+ if (Object.keys(where).length > 0) {
150
+ kwargs.where = where;
151
+ }
152
+
153
+ const results = await col.query(kwargs);
154
+ const docs = results.documents?.[0] ?? [];
155
+ const metas = results.metadatas?.[0] ?? [];
156
+ const dists = results.distances?.[0] ?? [];
157
+
158
+ const hits: SearchHit[] = [];
159
+ for (let i = 0; i < docs.length; i++) {
160
+ const doc = String(docs[i] ?? "");
161
+ const meta = (metas[i] ?? {}) as Record<string, unknown>;
162
+ const dist = Number(dists[i] ?? 0);
163
+
164
+ hits.push({
165
+ text: doc,
166
+ wing: String(meta.wing ?? "unknown"),
167
+ room: String(meta.room ?? "unknown"),
168
+ source_file: basename(String(meta.source_file ?? "?")),
169
+ similarity: Math.round((1 - dist) * 1000) / 1000,
170
+ });
171
+ }
172
+
173
+ return {
174
+ query,
175
+ filters: { wing, room },
176
+ results: hits,
177
+ };
178
+ } catch (error) {
179
+ return { error: `Search error: ${error}` };
180
+ }
181
+ }
@@ -0,0 +1,200 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { EntityRegistry } from "./entity-registry";
3
+
4
+ const SYSTEM_DICT_PATH = "/usr/share/dict/words";
5
+ const HAS_DIGIT = /\d/u;
6
+ const IS_CAMEL = /[A-Z][a-z]+[A-Z]/u;
7
+ const IS_ALLCAPS = /^(?:[A-Z_@#$%^&*()+={}|<>?.:/\\]|\[|\])+$/u;
8
+ const IS_TECHNICAL = /[-_]/u;
9
+ const IS_URL = /https?:\/\/|www\.|\/Users\/|~\/|\.[a-z]{2,4}$/iu;
10
+ const IS_CODE_OR_EMOJI = /[`*_#{}[\]\\]/u;
11
+ const TOKEN_RE = /(\S+)/gu;
12
+ const TRAILING_PUNCTUATION_RE = /[.,!?;:'")]+$/u;
13
+ const MIN_LENGTH = 4;
14
+
15
+ type Speller = (token: string) => string;
16
+
17
+ let systemWordsCache: ReadonlySet<string> | null = null;
18
+
19
+ function defaultSpeller(token: string): string {
20
+ // TODO: Plug in a Bun-compatible spellchecker when we choose one.
21
+ return token;
22
+ }
23
+
24
+ function shouldSkip(token: string, knownNames: ReadonlySet<string>): boolean {
25
+ if (token.length < MIN_LENGTH) {
26
+ return true;
27
+ }
28
+ if (HAS_DIGIT.test(token)) {
29
+ return true;
30
+ }
31
+ if (IS_CAMEL.test(token)) {
32
+ return true;
33
+ }
34
+ if (IS_ALLCAPS.test(token)) {
35
+ return true;
36
+ }
37
+ if (IS_TECHNICAL.test(token)) {
38
+ return true;
39
+ }
40
+ if (IS_URL.test(token)) {
41
+ return true;
42
+ }
43
+ if (IS_CODE_OR_EMOJI.test(token)) {
44
+ return true;
45
+ }
46
+ if (knownNames.has(token.toLowerCase())) {
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ function loadKnownNames(): Set<string> {
53
+ try {
54
+ const registry = EntityRegistry.load();
55
+ const names = new Set<string>();
56
+
57
+ for (const [canonical, info] of Object.entries(registry.people)) {
58
+ if (canonical) {
59
+ names.add(canonical.toLowerCase());
60
+ }
61
+
62
+ for (const alias of info.aliases) {
63
+ names.add(alias.toLowerCase());
64
+ }
65
+
66
+ if (typeof info.canonical === "string" && info.canonical) {
67
+ names.add(info.canonical.toLowerCase());
68
+ }
69
+ }
70
+
71
+ for (const project of registry.projects) {
72
+ names.add(project.toLowerCase());
73
+ }
74
+
75
+ return names;
76
+ } catch {
77
+ return new Set<string>();
78
+ }
79
+ }
80
+
81
+ function editDistance(a: string, b: string): number {
82
+ if (a === b) {
83
+ return 0;
84
+ }
85
+ if (a.length === 0) {
86
+ return b.length;
87
+ }
88
+ if (b.length === 0) {
89
+ return a.length;
90
+ }
91
+
92
+ let previousRow = Array.from({ length: b.length + 1 }, (_, index) => index);
93
+
94
+ for (let i = 0; i < a.length; i += 1) {
95
+ const currentRow = [i + 1];
96
+
97
+ for (let j = 0; j < b.length; j += 1) {
98
+ const substitutionCost = a[i] === b[j] ? 0 : 1;
99
+ currentRow.push(
100
+ Math.min(
101
+ previousRow[j + 1] + 1,
102
+ currentRow[j] + 1,
103
+ previousRow[j] + substitutionCost,
104
+ ),
105
+ );
106
+ }
107
+
108
+ previousRow = currentRow;
109
+ }
110
+
111
+ return previousRow[previousRow.length - 1];
112
+ }
113
+
114
+ function getSystemWords(): ReadonlySet<string> {
115
+ if (systemWordsCache !== null) {
116
+ return systemWordsCache;
117
+ }
118
+
119
+ if (!existsSync(SYSTEM_DICT_PATH)) {
120
+ systemWordsCache = new Set<string>();
121
+ return systemWordsCache;
122
+ }
123
+
124
+ const words = readFileSync(SYSTEM_DICT_PATH, "utf-8")
125
+ .split(/\r?\n/u)
126
+ .map((word) => word.trim().toLowerCase())
127
+ .filter((word) => word.length > 0);
128
+
129
+ systemWordsCache = new Set(words);
130
+ return systemWordsCache;
131
+ }
132
+
133
+ function splitTrailingPunctuation(token: string): { stripped: string; punctuation: string } {
134
+ const punctuation = token.match(TRAILING_PUNCTUATION_RE)?.[0] ?? "";
135
+ if (punctuation.length === 0) {
136
+ return { stripped: token, punctuation: "" };
137
+ }
138
+ return {
139
+ stripped: token.slice(0, token.length - punctuation.length),
140
+ punctuation,
141
+ };
142
+ }
143
+
144
+ export function spellcheckUserText(
145
+ text: string,
146
+ knownNames?: ReadonlySet<string>,
147
+ speller: Speller = defaultSpeller,
148
+ ): string {
149
+ const resolvedKnownNames = knownNames ?? loadKnownNames();
150
+ const systemWords = getSystemWords();
151
+
152
+ return text.replace(TOKEN_RE, (token) => {
153
+ const { stripped, punctuation } = splitTrailingPunctuation(token);
154
+ if (!stripped || shouldSkip(stripped, resolvedKnownNames)) {
155
+ return token;
156
+ }
157
+
158
+ if (stripped[0] !== stripped[0].toLowerCase()) {
159
+ return token;
160
+ }
161
+
162
+ if (systemWords.has(stripped.toLowerCase())) {
163
+ return token;
164
+ }
165
+
166
+ const corrected = speller(stripped);
167
+ if (corrected !== stripped) {
168
+ const distance = editDistance(stripped, corrected);
169
+ const maxEdits = stripped.length <= 7 ? 2 : 3;
170
+ if (distance > maxEdits) {
171
+ return token;
172
+ }
173
+ }
174
+
175
+ return `${corrected}${punctuation}`;
176
+ });
177
+ }
178
+
179
+ export function spellcheckTranscriptLine(line: string): string {
180
+ const stripped = line.trimStart();
181
+ if (!stripped.startsWith(">")) {
182
+ return line;
183
+ }
184
+
185
+ const leadingWhitespaceLength = line.length - stripped.length;
186
+ const prefixLength = leadingWhitespaceLength + (stripped.startsWith("> ") ? 2 : 1);
187
+ const message = line.slice(prefixLength);
188
+ if (message.trim().length === 0) {
189
+ return line;
190
+ }
191
+
192
+ return `${line.slice(0, prefixLength)}${spellcheckUserText(message)}`;
193
+ }
194
+
195
+ export function spellcheckTranscript(content: string): string {
196
+ return content
197
+ .split("\n")
198
+ .map((line) => spellcheckTranscriptLine(line))
199
+ .join("\n");
200
+ }
@@ -0,0 +1,8 @@
1
+ export type SplitMegaFilesOptions = {
2
+ dir: string;
3
+ outputDir?: string;
4
+ dryRun?: boolean;
5
+ minSessions: number;
6
+ };
7
+
8
+ export declare function splitMegaFiles(options: SplitMegaFilesOptions): void;
@@ -0,0 +1,297 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { basename, join, parse } from "node:path";
12
+
13
+ const HOME = homedir();
14
+ const LUMI_DIR = process.env.MEMPALACE_SOURCE_DIR ?? join(HOME, "Desktop/transcripts");
15
+ const KNOWN_NAMES_PATH = join(HOME, ".mempalace", "known_names.json");
16
+ const FALLBACK_PEOPLE = ["Alice", "Ben", "Riley", "Max", "Sam", "Devon", "Jordan"];
17
+ const TIMESTAMP_PATTERN = /⏺\s+(\d{1,2}:\d{2}\s+[AP]M)\s+\w+,\s+(\w+)\s+(\d{1,2}),\s+(\d{4})/;
18
+ const MONTHS: Record<string, string> = {
19
+ January: "01",
20
+ February: "02",
21
+ March: "03",
22
+ April: "04",
23
+ May: "05",
24
+ June: "06",
25
+ July: "07",
26
+ August: "08",
27
+ September: "09",
28
+ October: "10",
29
+ November: "11",
30
+ December: "12",
31
+ };
32
+ const SUBJECT_SKIP_PATTERN = /^(\.\/|cd |ls |python|bash|git |cat |source |export |claude|\.\/activate)/;
33
+
34
+ type KnownNamesConfig = {
35
+ names?: unknown;
36
+ username_map?: unknown;
37
+ };
38
+
39
+ type SplitMegaFilesOptions = {
40
+ dir?: string;
41
+ outputDir?: string;
42
+ dryRun?: boolean;
43
+ minSessions?: number;
44
+ file?: string;
45
+ };
46
+
47
+ function readKnownNamesConfig(): unknown {
48
+ if (!existsSync(KNOWN_NAMES_PATH)) {
49
+ return null;
50
+ }
51
+
52
+ try {
53
+ return JSON.parse(readFileSync(KNOWN_NAMES_PATH, "utf-8")) as unknown;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function isStringArray(value: unknown): value is string[] {
60
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
61
+ }
62
+
63
+ function isStringRecord(value: unknown): value is Record<string, string> {
64
+ return (
65
+ typeof value === "object"
66
+ && value !== null
67
+ && !Array.isArray(value)
68
+ && Object.values(value).every((item) => typeof item === "string")
69
+ );
70
+ }
71
+
72
+ function _loadKnownPeople(): string[] {
73
+ const data = readKnownNamesConfig();
74
+ if (isStringArray(data)) {
75
+ return data;
76
+ }
77
+
78
+ if (typeof data === "object" && data !== null) {
79
+ const config = data as KnownNamesConfig;
80
+ if (isStringArray(config.names)) {
81
+ return config.names;
82
+ }
83
+ }
84
+
85
+ return FALLBACK_PEOPLE;
86
+ }
87
+
88
+ function _loadUsernameMap(): Record<string, string> {
89
+ const data = readKnownNamesConfig();
90
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) {
91
+ const config = data as KnownNamesConfig;
92
+ if (isStringRecord(config.username_map)) {
93
+ return config.username_map;
94
+ }
95
+ }
96
+
97
+ return {};
98
+ }
99
+
100
+ const KNOWN_PEOPLE = _loadKnownPeople();
101
+
102
+ function escapeRegex(text: string): string {
103
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
104
+ }
105
+
106
+ function sanitizeFileComponent(text: string): string {
107
+ return text.replace(/[^\w.-]/g, "_").replace(/_+/g, "_");
108
+ }
109
+
110
+ export function isTrueSessionStart(lines: string[], idx: number): boolean {
111
+ const nearby = lines.slice(idx, idx + 6).join("");
112
+ return !nearby.includes("Ctrl+E") && !nearby.includes("previous messages");
113
+ }
114
+
115
+ export function findSessionBoundaries(lines: string[]): number[] {
116
+ const boundaries: number[] = [];
117
+ for (const [index, line] of lines.entries()) {
118
+ if (line.includes("Claude Code v") && isTrueSessionStart(lines, index)) {
119
+ boundaries.push(index);
120
+ }
121
+ }
122
+ return boundaries;
123
+ }
124
+
125
+ export function extractTimestamp(lines: string[]): [string | null, string | null] {
126
+ for (const line of lines.slice(0, 50)) {
127
+ const match = TIMESTAMP_PATTERN.exec(line);
128
+ if (!match) {
129
+ continue;
130
+ }
131
+
132
+ const [, timeStr, month, day, year] = match;
133
+ const monthValue = MONTHS[month] ?? "00";
134
+ const dayValue = day.padStart(2, "0");
135
+ const timeSafe = timeStr.replace(":", "").replace(" ", "");
136
+ const iso = `${year}-${monthValue}-${dayValue}`;
137
+ const human = `${year}-${monthValue}-${dayValue}_${timeSafe}`;
138
+ return [human, iso];
139
+ }
140
+
141
+ return [null, null];
142
+ }
143
+
144
+ export function extractPeople(lines: string[]): string[] {
145
+ const found = new Set<string>();
146
+ const text = lines.slice(0, 100).join("");
147
+
148
+ for (const person of KNOWN_PEOPLE) {
149
+ const pattern = new RegExp(`\\b${escapeRegex(person)}\\b`, "i");
150
+ if (pattern.test(text)) {
151
+ found.add(person);
152
+ }
153
+ }
154
+
155
+ const dirMatch = /\/Users\/(\w+)\//.exec(text);
156
+ if (dirMatch) {
157
+ const usernameMap = _loadUsernameMap();
158
+ const username = dirMatch[1];
159
+ const mappedName = usernameMap[username];
160
+ if (mappedName) {
161
+ found.add(mappedName);
162
+ }
163
+ }
164
+
165
+ return Array.from(found).sort((a, b) => a.localeCompare(b));
166
+ }
167
+
168
+ export function extractSubject(lines: string[]): string {
169
+ for (const line of lines) {
170
+ if (!line.startsWith("> ")) {
171
+ continue;
172
+ }
173
+
174
+ const prompt = line.slice(2).trim();
175
+ if (!prompt || SUBJECT_SKIP_PATTERN.test(prompt) || prompt.length <= 5) {
176
+ continue;
177
+ }
178
+
179
+ const subject = prompt.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").trim();
180
+ return subject.slice(0, 60) || "session";
181
+ }
182
+
183
+ return "session";
184
+ }
185
+
186
+ export function splitFile(filepath: string, outputDir?: string, dryRun = false): string[] {
187
+ const content = readFileSync(filepath, "utf-8");
188
+ const lines = content.split(/(?<=\n)/u);
189
+ const boundaries = findSessionBoundaries(lines);
190
+
191
+ if (boundaries.length < 2) {
192
+ return [];
193
+ }
194
+
195
+ boundaries.push(lines.length);
196
+ const outDir = outputDir ?? parse(filepath).dir;
197
+ const written: string[] = [];
198
+
199
+ if (!dryRun) {
200
+ mkdirSync(outDir, { recursive: true });
201
+ }
202
+
203
+ for (let index = 0; index < boundaries.length - 1; index += 1) {
204
+ const start = boundaries[index];
205
+ const end = boundaries[index + 1];
206
+ const chunk = lines.slice(start, end);
207
+
208
+ if (chunk.length < 10) {
209
+ continue;
210
+ }
211
+
212
+ const [timestampHuman] = extractTimestamp(chunk);
213
+ const people = extractPeople(chunk);
214
+ const subject = extractSubject(chunk);
215
+ const timestampPart = timestampHuman ?? `part${String(index + 1).padStart(2, "0")}`;
216
+ const peoplePart = people.length > 0 ? people.slice(0, 3).join("-") : "unknown";
217
+ const sourceStem = parse(filepath).name.replace(/[^\w-]/g, "_").slice(0, 40);
218
+ const filename = sanitizeFileComponent(
219
+ `${sourceStem}__${timestampPart}_${peoplePart}_${subject}.txt`,
220
+ );
221
+ const outPath = join(outDir, filename);
222
+
223
+ if (dryRun) {
224
+ console.log(` [${index + 1}/${boundaries.length - 1}] ${filename} (${chunk.length} lines)`);
225
+ } else {
226
+ writeFileSync(outPath, chunk.join(""));
227
+ console.log(` ✓ ${filename} (${chunk.length} lines)`);
228
+ }
229
+
230
+ written.push(outPath);
231
+ }
232
+
233
+ return written;
234
+ }
235
+
236
+ export function splitMegaFiles(options: SplitMegaFilesOptions): string[] {
237
+ const srcDir = options.dir ?? LUMI_DIR;
238
+ const outputDir = options.outputDir;
239
+ const dryRun = options.dryRun ?? false;
240
+ const minSessions = options.minSessions ?? 2;
241
+ const files = options.file
242
+ ? [options.file]
243
+ : readdirSync(srcDir)
244
+ .filter((name) => name.endsWith(".txt"))
245
+ .sort((a, b) => a.localeCompare(b))
246
+ .map((name) => join(srcDir, name));
247
+
248
+ const megaFiles: Array<{ file: string; sessions: number }> = [];
249
+
250
+ for (const file of files) {
251
+ const lines = readFileSync(file, "utf-8").split(/(?<=\n)/u);
252
+ const boundaries = findSessionBoundaries(lines);
253
+ if (boundaries.length >= minSessions) {
254
+ megaFiles.push({ file, sessions: boundaries.length });
255
+ }
256
+ }
257
+
258
+ if (megaFiles.length === 0) {
259
+ console.log(`No mega-files found in ${srcDir} (min ${minSessions} sessions).`);
260
+ return [];
261
+ }
262
+
263
+ console.log(`\n${"=".repeat(60)}`);
264
+ console.log(` Mega-file splitter — ${dryRun ? "DRY RUN" : "SPLITTING"}`);
265
+ console.log(`${"=".repeat(60)}`);
266
+ console.log(` Source: ${srcDir}`);
267
+ console.log(` Output: ${outputDir ?? "same dir as source"}`);
268
+ console.log(` Mega-files: ${megaFiles.length}`);
269
+ console.log(`${"─".repeat(60)}\n`);
270
+
271
+ const allWritten: string[] = [];
272
+
273
+ for (const megaFile of megaFiles) {
274
+ const sizeKb = Math.floor(statSync(megaFile.file).size / 1024);
275
+ console.log(` ${basename(megaFile.file)} (${megaFile.sessions} sessions, ${sizeKb}KB)`);
276
+ const written = splitFile(megaFile.file, outputDir, dryRun);
277
+ allWritten.push(...written);
278
+
279
+ if (!dryRun && written.length > 0) {
280
+ const backup = join(parse(megaFile.file).dir, `${parse(megaFile.file).name}.mega_backup`);
281
+ renameSync(megaFile.file, backup);
282
+ console.log(` → Original renamed to ${basename(backup)}\n`);
283
+ } else {
284
+ console.log("");
285
+ }
286
+ }
287
+
288
+ console.log(`${"─".repeat(60)}`);
289
+ if (dryRun) {
290
+ console.log(` DRY RUN — would create ${allWritten.length} files from ${megaFiles.length} mega-files`);
291
+ } else {
292
+ console.log(` Done — created ${allWritten.length} files from ${megaFiles.length} mega-files`);
293
+ }
294
+ console.log("");
295
+
296
+ return allWritten;
297
+ }