@hir4ta/mneme 0.23.0 → 0.23.2

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,784 @@
1
+ #!/usr/bin/env node
2
+
3
+ // lib/suppress-sqlite-warning.ts
4
+ var originalEmit = process.emit;
5
+ process.emit = (event, ...args) => {
6
+ if (event === "warning" && typeof args[0] === "object" && args[0] !== null && "name" in args[0] && args[0].name === "ExperimentalWarning" && "message" in args[0] && typeof args[0].message === "string" && args[0].message.includes("SQLite")) {
7
+ return false;
8
+ }
9
+ return originalEmit.apply(process, [event, ...args]);
10
+ };
11
+
12
+ // lib/save/index.ts
13
+ import * as fs5 from "node:fs";
14
+ import * as path4 from "node:path";
15
+
16
+ // lib/save/cleanup.ts
17
+ import * as fs2 from "node:fs";
18
+ import * as path2 from "node:path";
19
+
20
+ // lib/save/git.ts
21
+ import * as fs from "node:fs";
22
+ import * as os from "node:os";
23
+ import * as path from "node:path";
24
+ async function getGitInfo(projectPath) {
25
+ let owner = "unknown";
26
+ let repository = "";
27
+ let repositoryUrl = "";
28
+ let repositoryRoot = "";
29
+ try {
30
+ const { execSync } = await import("node:child_process");
31
+ owner = execSync("git config user.name", {
32
+ encoding: "utf8",
33
+ cwd: projectPath,
34
+ stdio: ["pipe", "pipe", "pipe"]
35
+ }).trim() || owner;
36
+ repositoryRoot = execSync("git rev-parse --show-toplevel", {
37
+ encoding: "utf8",
38
+ cwd: projectPath,
39
+ stdio: ["pipe", "pipe", "pipe"]
40
+ }).trim() || "";
41
+ repositoryUrl = execSync("git remote get-url origin", {
42
+ encoding: "utf8",
43
+ cwd: projectPath,
44
+ stdio: ["pipe", "pipe", "pipe"]
45
+ }).trim() || "";
46
+ const match = repositoryUrl.match(/[:/]([^/]+\/[^/]+?)(\.git)?$/);
47
+ if (match) {
48
+ repository = match[1].replace(/\.git$/, "");
49
+ }
50
+ } catch {
51
+ try {
52
+ owner = os.userInfo().username || owner;
53
+ } catch {
54
+ }
55
+ }
56
+ return { owner, repository, repositoryUrl, repositoryRoot };
57
+ }
58
+ function resolveMnemeSessionId(projectPath, claudeSessionId) {
59
+ const shortId = claudeSessionId.slice(0, 8);
60
+ const sessionLinkPath = path.join(
61
+ projectPath,
62
+ ".mneme",
63
+ "session-links",
64
+ `${shortId}.json`
65
+ );
66
+ if (fs.existsSync(sessionLinkPath)) {
67
+ try {
68
+ const link = JSON.parse(fs.readFileSync(sessionLinkPath, "utf8"));
69
+ if (link.masterSessionId) {
70
+ return link.masterSessionId;
71
+ }
72
+ } catch {
73
+ }
74
+ }
75
+ return shortId;
76
+ }
77
+ function findSessionFileById(projectPath, mnemeSessionId) {
78
+ const sessionsDir = path.join(projectPath, ".mneme", "sessions");
79
+ const searchDir = (dir) => {
80
+ if (!fs.existsSync(dir)) return null;
81
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
82
+ const fullPath = path.join(dir, entry.name);
83
+ if (entry.isDirectory()) {
84
+ const result = searchDir(fullPath);
85
+ if (result) return result;
86
+ } else if (entry.name === `${mnemeSessionId}.json`) {
87
+ return fullPath;
88
+ }
89
+ }
90
+ return null;
91
+ };
92
+ return searchDir(sessionsDir);
93
+ }
94
+ function hasSessionSummary(sessionFile) {
95
+ if (!sessionFile) return false;
96
+ try {
97
+ const session = JSON.parse(fs.readFileSync(sessionFile, "utf8"));
98
+ return !!session.summary;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ // lib/save/cleanup.ts
105
+ var { DatabaseSync } = await import("node:sqlite");
106
+ function cleanupUncommittedSession(claudeSessionId, projectPath) {
107
+ const dbPath = path2.join(projectPath, ".mneme", "local.db");
108
+ if (!fs2.existsSync(dbPath)) return { deleted: false, count: 0 };
109
+ const db = new DatabaseSync(dbPath);
110
+ try {
111
+ const stateStmt = db.prepare(
112
+ "SELECT is_committed FROM session_save_state WHERE claude_session_id = ?"
113
+ );
114
+ const state = stateStmt.get(claudeSessionId);
115
+ if (state?.is_committed === 1) return { deleted: false, count: 0 };
116
+ const mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
117
+ const sessionFile = findSessionFileById(projectPath, mnemeSessionId);
118
+ if (hasSessionSummary(sessionFile)) return { deleted: false, count: 0 };
119
+ const countStmt = db.prepare(
120
+ "SELECT COUNT(*) as count FROM interactions WHERE claude_session_id = ?"
121
+ );
122
+ const countResult = countStmt.get(claudeSessionId);
123
+ const count = countResult?.count || 0;
124
+ if (count > 0) {
125
+ db.prepare("DELETE FROM interactions WHERE claude_session_id = ?").run(
126
+ claudeSessionId
127
+ );
128
+ }
129
+ db.prepare(
130
+ "DELETE FROM session_save_state WHERE claude_session_id = ?"
131
+ ).run(claudeSessionId);
132
+ return { deleted: true, count };
133
+ } catch (error) {
134
+ console.error(`[mneme] Error cleaning up session: ${error}`);
135
+ return { deleted: false, count: 0 };
136
+ } finally {
137
+ db.close();
138
+ }
139
+ }
140
+ function cleanupStaleUncommittedSessions(projectPath, graceDays) {
141
+ const dbPath = path2.join(projectPath, ".mneme", "local.db");
142
+ if (!fs2.existsSync(dbPath))
143
+ return { deletedSessions: 0, deletedInteractions: 0 };
144
+ const db = new DatabaseSync(dbPath);
145
+ let deletedSessions = 0;
146
+ let deletedInteractions = 0;
147
+ const normalizedGraceDays = Math.max(1, Math.floor(graceDays));
148
+ try {
149
+ const staleRows = db.prepare(
150
+ `SELECT claude_session_id, mneme_session_id FROM session_save_state
151
+ WHERE is_committed = 0 AND updated_at <= datetime('now', ?)`
152
+ ).all(`-${normalizedGraceDays} days`);
153
+ if (staleRows.length === 0)
154
+ return { deletedSessions: 0, deletedInteractions: 0 };
155
+ const deleteInteractionStmt = db.prepare(
156
+ "DELETE FROM interactions WHERE claude_session_id = ?"
157
+ );
158
+ const countInteractionStmt = db.prepare(
159
+ "SELECT COUNT(*) as count FROM interactions WHERE claude_session_id = ?"
160
+ );
161
+ const deleteStateStmt = db.prepare(
162
+ "DELETE FROM session_save_state WHERE claude_session_id = ?"
163
+ );
164
+ for (const row of staleRows) {
165
+ const sessionFile = findSessionFileById(
166
+ projectPath,
167
+ row.mneme_session_id
168
+ );
169
+ if (hasSessionSummary(sessionFile)) continue;
170
+ const countResult = countInteractionStmt.get(row.claude_session_id);
171
+ const count = countResult?.count || 0;
172
+ if (count > 0) {
173
+ deleteInteractionStmt.run(row.claude_session_id);
174
+ deletedInteractions += count;
175
+ }
176
+ deleteStateStmt.run(row.claude_session_id);
177
+ if (sessionFile && fs2.existsSync(sessionFile)) {
178
+ try {
179
+ fs2.unlinkSync(sessionFile);
180
+ deletedSessions += 1;
181
+ } catch {
182
+ }
183
+ }
184
+ const linkPath = path2.join(
185
+ projectPath,
186
+ ".mneme",
187
+ "session-links",
188
+ `${row.claude_session_id.slice(0, 8)}.json`
189
+ );
190
+ if (fs2.existsSync(linkPath)) {
191
+ try {
192
+ fs2.unlinkSync(linkPath);
193
+ } catch {
194
+ }
195
+ }
196
+ }
197
+ return { deletedSessions, deletedInteractions };
198
+ } catch (error) {
199
+ console.error(`[mneme] Error cleaning stale sessions: ${error}`);
200
+ return { deletedSessions: 0, deletedInteractions: 0 };
201
+ } finally {
202
+ db.close();
203
+ }
204
+ }
205
+
206
+ // lib/save/db.ts
207
+ import * as fs3 from "node:fs";
208
+ import * as path3 from "node:path";
209
+ var { DatabaseSync: DatabaseSync2 } = await import("node:sqlite");
210
+ function getSchemaPath() {
211
+ const scriptDir = path3.dirname(new URL(import.meta.url).pathname);
212
+ const candidates = [
213
+ path3.join(scriptDir, "..", "schema.sql"),
214
+ path3.join(scriptDir, "schema.sql"),
215
+ path3.join(scriptDir, "..", "..", "lib", "schema.sql")
216
+ ];
217
+ for (const candidate of candidates) {
218
+ if (fs3.existsSync(candidate)) {
219
+ return candidate;
220
+ }
221
+ }
222
+ return null;
223
+ }
224
+ var FALLBACK_SCHEMA = `
225
+ CREATE TABLE IF NOT EXISTS interactions (
226
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
227
+ session_id TEXT NOT NULL,
228
+ claude_session_id TEXT,
229
+ project_path TEXT NOT NULL,
230
+ repository TEXT,
231
+ repository_url TEXT,
232
+ repository_root TEXT,
233
+ owner TEXT NOT NULL,
234
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
235
+ content TEXT NOT NULL,
236
+ thinking TEXT,
237
+ tool_calls TEXT,
238
+ timestamp TEXT NOT NULL,
239
+ is_compact_summary INTEGER DEFAULT 0,
240
+ agent_id TEXT,
241
+ agent_type TEXT,
242
+ created_at TEXT DEFAULT (datetime('now'))
243
+ );
244
+ CREATE INDEX IF NOT EXISTS idx_interactions_session ON interactions(session_id);
245
+ CREATE INDEX IF NOT EXISTS idx_interactions_claude_session ON interactions(claude_session_id);
246
+ CREATE INDEX IF NOT EXISTS idx_interactions_owner ON interactions(owner);
247
+ CREATE INDEX IF NOT EXISTS idx_interactions_timestamp ON interactions(timestamp);
248
+ CREATE INDEX IF NOT EXISTS idx_interactions_project ON interactions(project_path);
249
+ CREATE INDEX IF NOT EXISTS idx_interactions_repository ON interactions(repository);
250
+
251
+ CREATE TABLE IF NOT EXISTS session_save_state (
252
+ claude_session_id TEXT PRIMARY KEY,
253
+ mneme_session_id TEXT NOT NULL,
254
+ project_path TEXT NOT NULL,
255
+ last_saved_timestamp TEXT,
256
+ last_saved_line INTEGER DEFAULT 0,
257
+ is_committed INTEGER DEFAULT 0,
258
+ created_at TEXT DEFAULT (datetime('now')),
259
+ updated_at TEXT DEFAULT (datetime('now'))
260
+ );
261
+ CREATE INDEX IF NOT EXISTS idx_save_state_mneme_session ON session_save_state(mneme_session_id);
262
+ CREATE INDEX IF NOT EXISTS idx_save_state_project ON session_save_state(project_path);
263
+
264
+ CREATE TABLE IF NOT EXISTS pre_compact_backups (
265
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
266
+ session_id TEXT NOT NULL,
267
+ project_path TEXT NOT NULL,
268
+ owner TEXT NOT NULL,
269
+ interactions TEXT NOT NULL,
270
+ created_at TEXT DEFAULT (datetime('now'))
271
+ );
272
+
273
+ CREATE VIRTUAL TABLE IF NOT EXISTS interactions_fts USING fts5(
274
+ content,
275
+ thinking,
276
+ content=interactions,
277
+ content_rowid=id,
278
+ tokenize='unicode61'
279
+ );
280
+
281
+ CREATE TRIGGER IF NOT EXISTS interactions_ai AFTER INSERT ON interactions BEGIN
282
+ INSERT INTO interactions_fts(rowid, content, thinking)
283
+ VALUES (new.id, new.content, new.thinking);
284
+ END;
285
+
286
+ CREATE TRIGGER IF NOT EXISTS interactions_ad AFTER DELETE ON interactions BEGIN
287
+ INSERT INTO interactions_fts(interactions_fts, rowid, content, thinking)
288
+ VALUES ('delete', old.id, old.content, old.thinking);
289
+ END;
290
+ `;
291
+ function migrateDatabase(db) {
292
+ try {
293
+ const columns = db.prepare("PRAGMA table_info(interactions)").all();
294
+ const hasClaudeSessionId = columns.some(
295
+ (c) => c.name === "claude_session_id"
296
+ );
297
+ if (!hasClaudeSessionId) {
298
+ db.exec("ALTER TABLE interactions ADD COLUMN claude_session_id TEXT");
299
+ db.exec(
300
+ "CREATE INDEX IF NOT EXISTS idx_interactions_claude_session ON interactions(claude_session_id)"
301
+ );
302
+ console.error("[mneme] Migrated: added claude_session_id column");
303
+ }
304
+ } catch {
305
+ }
306
+ try {
307
+ db.exec("SELECT 1 FROM session_save_state LIMIT 1");
308
+ } catch {
309
+ db.exec(`
310
+ CREATE TABLE IF NOT EXISTS session_save_state (
311
+ claude_session_id TEXT PRIMARY KEY,
312
+ mneme_session_id TEXT NOT NULL,
313
+ project_path TEXT NOT NULL,
314
+ last_saved_timestamp TEXT,
315
+ last_saved_line INTEGER DEFAULT 0,
316
+ is_committed INTEGER DEFAULT 0,
317
+ created_at TEXT DEFAULT (datetime('now')),
318
+ updated_at TEXT DEFAULT (datetime('now'))
319
+ );
320
+ CREATE INDEX IF NOT EXISTS idx_save_state_mneme_session ON session_save_state(mneme_session_id);
321
+ CREATE INDEX IF NOT EXISTS idx_save_state_project ON session_save_state(project_path);
322
+ `);
323
+ console.error("[mneme] Migrated: created session_save_state table");
324
+ }
325
+ }
326
+ function initDatabase(dbPath) {
327
+ const mnemeDir = path3.dirname(dbPath);
328
+ if (!fs3.existsSync(mnemeDir)) {
329
+ fs3.mkdirSync(mnemeDir, { recursive: true });
330
+ }
331
+ const db = new DatabaseSync2(dbPath);
332
+ try {
333
+ db.exec("SELECT 1 FROM interactions LIMIT 1");
334
+ } catch {
335
+ const schemaPath = getSchemaPath();
336
+ if (schemaPath) {
337
+ const schema = fs3.readFileSync(schemaPath, "utf8");
338
+ db.exec(schema);
339
+ console.error(`[mneme] Database initialized from schema: ${dbPath}`);
340
+ } else {
341
+ db.exec(FALLBACK_SCHEMA);
342
+ console.error(
343
+ `[mneme] Database initialized with fallback schema: ${dbPath}`
344
+ );
345
+ }
346
+ }
347
+ migrateDatabase(db);
348
+ db.exec("PRAGMA journal_mode = WAL");
349
+ db.exec("PRAGMA busy_timeout = 5000");
350
+ db.exec("PRAGMA synchronous = NORMAL");
351
+ return db;
352
+ }
353
+ function getSaveState(db, claudeSessionId, mnemeSessionId, projectPath) {
354
+ const stmt = db.prepare(
355
+ "SELECT * FROM session_save_state WHERE claude_session_id = ?"
356
+ );
357
+ const row = stmt.get(claudeSessionId);
358
+ if (row) {
359
+ return {
360
+ claudeSessionId: row.claude_session_id,
361
+ mnemeSessionId: row.mneme_session_id,
362
+ projectPath: row.project_path,
363
+ lastSavedTimestamp: row.last_saved_timestamp,
364
+ lastSavedLine: row.last_saved_line,
365
+ isCommitted: row.is_committed
366
+ };
367
+ }
368
+ const insertStmt = db.prepare(`
369
+ INSERT INTO session_save_state (claude_session_id, mneme_session_id, project_path)
370
+ VALUES (?, ?, ?)
371
+ `);
372
+ insertStmt.run(claudeSessionId, mnemeSessionId, projectPath);
373
+ return {
374
+ claudeSessionId,
375
+ mnemeSessionId,
376
+ projectPath,
377
+ lastSavedTimestamp: null,
378
+ lastSavedLine: 0,
379
+ isCommitted: 0
380
+ };
381
+ }
382
+ function updateSaveState(db, claudeSessionId, lastSavedTimestamp, lastSavedLine) {
383
+ const stmt = db.prepare(`
384
+ UPDATE session_save_state
385
+ SET last_saved_timestamp = ?, last_saved_line = ?, updated_at = datetime('now')
386
+ WHERE claude_session_id = ?
387
+ `);
388
+ stmt.run(lastSavedTimestamp, lastSavedLine, claudeSessionId);
389
+ }
390
+
391
+ // lib/save/parser.ts
392
+ import * as fs4 from "node:fs";
393
+ import * as readline from "node:readline";
394
+ function extractSlashCommand(content) {
395
+ const match = content.match(/<command-name>([^<]+)<\/command-name>/);
396
+ return match ? match[1] : void 0;
397
+ }
398
+ function extractToolResultMeta(content, toolUseIdToName, toolUseIdToFilePath) {
399
+ return content.filter((c) => c.type === "tool_result" && c.tool_use_id).map((c) => {
400
+ const contentStr = typeof c.content === "string" ? c.content : c.content ? JSON.stringify(c.content) : "";
401
+ const lineCount = contentStr.split("\n").length;
402
+ const toolUseId = c.tool_use_id || "";
403
+ let filePath = toolUseIdToFilePath.get(toolUseId);
404
+ if (!filePath) {
405
+ const filePathMatch = contentStr.match(
406
+ /(?:^|\s)((?:\/|\.\/)\S+\.\w+)\b/
407
+ );
408
+ filePath = filePathMatch ? filePathMatch[1] : void 0;
409
+ }
410
+ return {
411
+ toolUseId,
412
+ toolName: toolUseIdToName.get(toolUseId),
413
+ success: !c.is_error,
414
+ contentLength: contentStr.length,
415
+ lineCount: lineCount > 1 ? lineCount : void 0,
416
+ filePath
417
+ };
418
+ });
419
+ }
420
+ async function parseTranscriptIncremental(transcriptPath, lastSavedLine) {
421
+ const fileStream = fs4.createReadStream(transcriptPath);
422
+ const rl = readline.createInterface({
423
+ input: fileStream,
424
+ crlfDelay: Number.POSITIVE_INFINITY
425
+ });
426
+ const entries = [];
427
+ let lineNumber = 0;
428
+ for await (const line of rl) {
429
+ lineNumber++;
430
+ if (lineNumber <= lastSavedLine) continue;
431
+ if (line.trim()) {
432
+ try {
433
+ entries.push(JSON.parse(line));
434
+ } catch {
435
+ }
436
+ }
437
+ }
438
+ const planModeEvents = [];
439
+ for (const entry of entries) {
440
+ if (entry.type === "assistant" && Array.isArray(entry.message?.content)) {
441
+ for (const c of entry.message.content) {
442
+ if (c.type === "tool_use") {
443
+ if (c.name === "EnterPlanMode") {
444
+ planModeEvents.push({ timestamp: entry.timestamp, entering: true });
445
+ } else if (c.name === "ExitPlanMode") {
446
+ planModeEvents.push({
447
+ timestamp: entry.timestamp,
448
+ entering: false
449
+ });
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
455
+ function isInPlanMode(timestamp) {
456
+ let inPlanMode = false;
457
+ for (const event of planModeEvents) {
458
+ if (event.timestamp > timestamp) break;
459
+ inPlanMode = event.entering;
460
+ }
461
+ return inPlanMode;
462
+ }
463
+ const progressEvents = /* @__PURE__ */ new Map();
464
+ for (const entry of entries) {
465
+ if (entry.type === "progress" && entry.data?.type) {
466
+ if (entry.data.type === "hook_progress") continue;
467
+ const event = {
468
+ type: entry.data.type,
469
+ timestamp: entry.timestamp,
470
+ hookEvent: entry.data.hookEvent,
471
+ hookName: entry.data.hookName,
472
+ toolName: entry.data.toolName,
473
+ ...entry.data.type === "agent_progress" && {
474
+ prompt: entry.data.prompt,
475
+ agentId: entry.data.agentId
476
+ }
477
+ };
478
+ const key = entry.timestamp.slice(0, 16);
479
+ if (!progressEvents.has(key)) progressEvents.set(key, []);
480
+ progressEvents.get(key)?.push(event);
481
+ }
482
+ }
483
+ const userMessages = entries.filter((e) => {
484
+ if (e.type !== "user" || e.message?.role !== "user") return false;
485
+ if (e.isMeta === true) return false;
486
+ const content = e.message?.content;
487
+ if (typeof content !== "string") return false;
488
+ if (content.startsWith("<local-command-stdout>")) return false;
489
+ if (content.startsWith("<local-command-caveat>")) return false;
490
+ return true;
491
+ }).map((e) => {
492
+ const content = e.message?.content;
493
+ return {
494
+ timestamp: e.timestamp,
495
+ content,
496
+ isCompactSummary: e.isCompactSummary || false,
497
+ slashCommand: extractSlashCommand(content)
498
+ };
499
+ });
500
+ const toolUseIdToName = /* @__PURE__ */ new Map();
501
+ const toolUseIdToFilePath = /* @__PURE__ */ new Map();
502
+ for (const entry of entries) {
503
+ if (entry.type === "assistant" && Array.isArray(entry.message?.content)) {
504
+ for (const c of entry.message.content) {
505
+ if (c.type === "tool_use" && c.id && c.name) {
506
+ toolUseIdToName.set(c.id, c.name);
507
+ if (c.input?.file_path) {
508
+ toolUseIdToFilePath.set(c.id, c.input.file_path);
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ const toolResultsByTimestamp = /* @__PURE__ */ new Map();
515
+ for (const entry of entries) {
516
+ if (entry.type === "user" && Array.isArray(entry.message?.content)) {
517
+ const results = extractToolResultMeta(
518
+ entry.message.content,
519
+ toolUseIdToName,
520
+ toolUseIdToFilePath
521
+ );
522
+ if (results.length > 0) {
523
+ const key = entry.timestamp.slice(0, 16);
524
+ const existing = toolResultsByTimestamp.get(key) || [];
525
+ toolResultsByTimestamp.set(key, [...existing, ...results]);
526
+ }
527
+ }
528
+ }
529
+ const assistantMessages = entries.filter((e) => e.type === "assistant").map((e) => {
530
+ const contentArray = e.message?.content;
531
+ if (!Array.isArray(contentArray)) return null;
532
+ const thinking = contentArray.filter((c) => c.type === "thinking" && c.thinking).map((c) => c.thinking).join("\n");
533
+ const text = contentArray.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
534
+ const toolDetails = contentArray.filter((c) => c.type === "tool_use" && c.name).map((c) => ({
535
+ name: c.name ?? "",
536
+ detail: c.name === "Bash" ? c.input?.command : c.name === "Read" || c.name === "Edit" || c.name === "Write" ? c.input?.file_path : c.name === "Glob" || c.name === "Grep" ? c.input?.pattern : null
537
+ }));
538
+ if (!thinking && !text && toolDetails.length === 0) return null;
539
+ return { timestamp: e.timestamp, thinking, text, toolDetails };
540
+ }).filter((m) => m !== null);
541
+ const interactions = [];
542
+ for (let i = 0; i < userMessages.length; i++) {
543
+ const user = userMessages[i];
544
+ const nextUserTs = i + 1 < userMessages.length ? userMessages[i + 1].timestamp : "9999-12-31T23:59:59Z";
545
+ const turnResponses = assistantMessages.filter(
546
+ (a) => a.timestamp >= user.timestamp && a.timestamp < nextUserTs
547
+ );
548
+ if (turnResponses.length > 0) {
549
+ const allToolDetails = turnResponses.flatMap((r) => r.toolDetails);
550
+ const timeKey = user.timestamp.slice(0, 16);
551
+ interactions.push({
552
+ timestamp: user.timestamp,
553
+ user: user.content,
554
+ thinking: turnResponses.filter((r) => r.thinking).map((r) => r.thinking).join("\n"),
555
+ assistant: turnResponses.filter((r) => r.text).map((r) => r.text).join("\n"),
556
+ isCompactSummary: user.isCompactSummary,
557
+ toolsUsed: [...new Set(allToolDetails.map((t) => t.name))],
558
+ toolDetails: allToolDetails,
559
+ inPlanMode: isInPlanMode(user.timestamp) || void 0,
560
+ slashCommand: user.slashCommand,
561
+ toolResults: toolResultsByTimestamp.get(timeKey),
562
+ progressEvents: progressEvents.get(timeKey)
563
+ });
564
+ }
565
+ }
566
+ return { interactions, totalLines: lineNumber };
567
+ }
568
+
569
+ // lib/save/index.ts
570
+ var { DatabaseSync: DatabaseSync3 } = await import("node:sqlite");
571
+ async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
572
+ if (!claudeSessionId || !transcriptPath || !projectPath) {
573
+ return {
574
+ success: false,
575
+ savedCount: 0,
576
+ totalCount: 0,
577
+ message: "Missing required parameters"
578
+ };
579
+ }
580
+ if (!fs5.existsSync(transcriptPath)) {
581
+ return {
582
+ success: false,
583
+ savedCount: 0,
584
+ totalCount: 0,
585
+ message: `Transcript not found: ${transcriptPath}`
586
+ };
587
+ }
588
+ const dbPath = path4.join(projectPath, ".mneme", "local.db");
589
+ const db = initDatabase(dbPath);
590
+ const mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
591
+ const saveState = getSaveState(
592
+ db,
593
+ claudeSessionId,
594
+ mnemeSessionId,
595
+ projectPath
596
+ );
597
+ const { interactions, totalLines } = await parseTranscriptIncremental(
598
+ transcriptPath,
599
+ saveState.lastSavedLine
600
+ );
601
+ if (interactions.length === 0) {
602
+ return {
603
+ success: true,
604
+ savedCount: 0,
605
+ totalCount: totalLines,
606
+ message: "No new interactions to save"
607
+ };
608
+ }
609
+ const { owner, repository, repositoryUrl, repositoryRoot } = await getGitInfo(projectPath);
610
+ const insertStmt = db.prepare(`
611
+ INSERT INTO interactions (
612
+ session_id, claude_session_id, project_path, repository, repository_url, repository_root,
613
+ owner, role, content, thinking, tool_calls, timestamp, is_compact_summary
614
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
615
+ `);
616
+ let insertedCount = 0;
617
+ let lastTimestamp = saveState.lastSavedTimestamp || "";
618
+ for (const interaction of interactions) {
619
+ try {
620
+ const metadata = JSON.stringify({
621
+ toolsUsed: interaction.toolsUsed,
622
+ toolDetails: interaction.toolDetails,
623
+ ...interaction.inPlanMode && { inPlanMode: true },
624
+ ...interaction.slashCommand && {
625
+ slashCommand: interaction.slashCommand
626
+ },
627
+ ...interaction.toolResults?.length && {
628
+ toolResults: interaction.toolResults
629
+ },
630
+ ...interaction.progressEvents?.length && {
631
+ progressEvents: interaction.progressEvents
632
+ }
633
+ });
634
+ insertStmt.run(
635
+ mnemeSessionId,
636
+ claudeSessionId,
637
+ projectPath,
638
+ repository,
639
+ repositoryUrl,
640
+ repositoryRoot,
641
+ owner,
642
+ "user",
643
+ interaction.user,
644
+ null,
645
+ metadata,
646
+ interaction.timestamp,
647
+ interaction.isCompactSummary ? 1 : 0
648
+ );
649
+ insertedCount++;
650
+ if (interaction.assistant) {
651
+ const assistantMetadata = JSON.stringify({
652
+ toolsUsed: interaction.toolsUsed,
653
+ toolDetails: interaction.toolDetails,
654
+ ...interaction.inPlanMode && { inPlanMode: true },
655
+ ...interaction.toolResults?.length && {
656
+ toolResults: interaction.toolResults
657
+ },
658
+ ...interaction.progressEvents?.length && {
659
+ progressEvents: interaction.progressEvents
660
+ }
661
+ });
662
+ insertStmt.run(
663
+ mnemeSessionId,
664
+ claudeSessionId,
665
+ projectPath,
666
+ repository,
667
+ repositoryUrl,
668
+ repositoryRoot,
669
+ owner,
670
+ "assistant",
671
+ interaction.assistant,
672
+ interaction.thinking || null,
673
+ assistantMetadata,
674
+ interaction.timestamp,
675
+ 0
676
+ );
677
+ insertedCount++;
678
+ }
679
+ lastTimestamp = interaction.timestamp;
680
+ } catch (error) {
681
+ console.error(`[mneme] Error inserting interaction: ${error}`);
682
+ }
683
+ }
684
+ updateSaveState(db, claudeSessionId, lastTimestamp, totalLines);
685
+ db.close();
686
+ return {
687
+ success: true,
688
+ savedCount: insertedCount,
689
+ totalCount: totalLines,
690
+ message: `Saved ${insertedCount} messages (${interactions.length} turns)`
691
+ };
692
+ }
693
+ function markSessionCommitted(claudeSessionId, projectPath) {
694
+ const dbPath = path4.join(projectPath, ".mneme", "local.db");
695
+ if (!fs5.existsSync(dbPath)) return false;
696
+ const db = new DatabaseSync3(dbPath);
697
+ try {
698
+ const stmt = db.prepare(`
699
+ UPDATE session_save_state
700
+ SET is_committed = 1, updated_at = datetime('now')
701
+ WHERE claude_session_id = ?
702
+ `);
703
+ stmt.run(claudeSessionId);
704
+ return true;
705
+ } catch {
706
+ return false;
707
+ } finally {
708
+ db.close();
709
+ }
710
+ }
711
+ async function main() {
712
+ const args = process.argv.slice(2);
713
+ const getArg = (name) => {
714
+ const index = args.indexOf(`--${name}`);
715
+ return index !== -1 ? args[index + 1] : void 0;
716
+ };
717
+ const command = args[0];
718
+ if (command === "save") {
719
+ const sessionId = getArg("session");
720
+ const transcriptPath = getArg("transcript");
721
+ const projectPath = getArg("project");
722
+ if (!sessionId || !transcriptPath || !projectPath) {
723
+ console.error(
724
+ "Usage: incremental-save.js save --session <id> --transcript <path> --project <path>"
725
+ );
726
+ process.exit(1);
727
+ }
728
+ const result = await incrementalSave(
729
+ sessionId,
730
+ transcriptPath,
731
+ projectPath
732
+ );
733
+ console.log(JSON.stringify(result));
734
+ process.exit(result.success ? 0 : 1);
735
+ } else if (command === "commit") {
736
+ const sessionId = getArg("session");
737
+ const projectPath = getArg("project");
738
+ if (!sessionId || !projectPath) {
739
+ console.error(
740
+ "Usage: incremental-save.js commit --session <id> --project <path>"
741
+ );
742
+ process.exit(1);
743
+ }
744
+ const success = markSessionCommitted(sessionId, projectPath);
745
+ console.log(JSON.stringify({ success }));
746
+ process.exit(success ? 0 : 1);
747
+ } else if (command === "cleanup") {
748
+ const sessionId = getArg("session");
749
+ const projectPath = getArg("project");
750
+ if (!sessionId || !projectPath) {
751
+ console.error(
752
+ "Usage: incremental-save.js cleanup --session <id> --project <path>"
753
+ );
754
+ process.exit(1);
755
+ }
756
+ const result = cleanupUncommittedSession(sessionId, projectPath);
757
+ console.log(JSON.stringify(result));
758
+ process.exit(0);
759
+ } else if (command === "cleanup-stale") {
760
+ const projectPath = getArg("project");
761
+ const graceDays = Number.parseInt(getArg("grace-days") || "7", 10);
762
+ if (!projectPath) {
763
+ console.error(
764
+ "Usage: incremental-save.js cleanup-stale --project <path> [--grace-days <n>]"
765
+ );
766
+ process.exit(1);
767
+ }
768
+ const result = cleanupStaleUncommittedSessions(projectPath, graceDays);
769
+ console.log(JSON.stringify(result));
770
+ process.exit(0);
771
+ } else {
772
+ console.error("Commands: save, commit, cleanup");
773
+ process.exit(1);
774
+ }
775
+ }
776
+ if (import.meta.url === `file://${process.argv[1]}`) {
777
+ main();
778
+ }
779
+ export {
780
+ cleanupStaleUncommittedSessions,
781
+ cleanupUncommittedSession,
782
+ incrementalSave,
783
+ markSessionCommitted
784
+ };