claude-memory-layer 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +3577 -389
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1383 -138
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1917 -214
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1813 -231
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1802 -205
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1909 -248
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1861 -206
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +2341 -217
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +2350 -226
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1805 -206
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +1447 -55
- package/dist/ui/index.html +318 -147
- package/dist/ui/style.css +892 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -0
- package/docs/OPERATIONS.md +18 -0
- package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
- package/memory/_index.md +405 -0
- package/memory/default/uncategorized/2026-02-25.md +4839 -0
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
- package/memory/specs/citations-system/2026-02-25.md +1121 -0
- package/memory/specs/endless-mode/2026-02-25.md +1392 -0
- package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
- package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
- package/memory/specs/private-tags/2026-02-25.md +1057 -0
- package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
- package/memory/specs/task-entity-system/2026-02-25.md +924 -0
- package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
- package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
- package/package.json +9 -2
- package/scripts/build.ts +6 -0
- package/scripts/fix-sync-gap.js +32 -0
- package/scripts/heartbeat-memory-orchestrator.sh +28 -0
- package/scripts/report-sync-gap.js +26 -0
- package/scripts/review-queue-auto-resolve.js +21 -0
- package/scripts/sync-gap-auto-heal.sh +17 -0
- package/specs/20260207-dashboard-upgrade/context.md +38 -0
- package/specs/20260207-dashboard-upgrade/spec.md +96 -0
- package/src/cli/index.ts +391 -60
- package/src/core/consolidated-store.ts +63 -1
- package/src/core/consolidation-worker.ts +115 -6
- package/src/core/event-store.ts +14 -0
- package/src/core/index.ts +1 -0
- package/src/core/ingest-interceptor.ts +80 -0
- package/src/core/markdown-mirror.ts +70 -0
- package/src/core/md-mirror.ts +92 -0
- package/src/core/mongo-sync-config.ts +165 -0
- package/src/core/mongo-sync-worker.ts +381 -0
- package/src/core/retriever.ts +540 -150
- package/src/core/sqlite-event-store.ts +794 -7
- package/src/core/sqlite-wrapper.ts +8 -0
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/turn-state.ts +159 -0
- package/src/core/types.ts +51 -8
- package/src/core/vector-store.ts +21 -3
- package/src/hooks/post-tool-use.ts +68 -23
- package/src/hooks/session-end.ts +8 -3
- package/src/hooks/stop.ts +96 -25
- package/src/hooks/user-prompt-submit.ts +44 -5
- package/src/server/api/chat.ts +244 -0
- package/src/server/api/citations.ts +3 -3
- package/src/server/api/events.ts +30 -5
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +9 -1
- package/src/server/api/projects.ts +74 -0
- package/src/server/api/search.ts +3 -3
- package/src/server/api/sessions.ts +3 -3
- package/src/server/api/stats.ts +89 -8
- package/src/server/api/turns.ts +143 -0
- package/src/server/api/utils.ts +46 -0
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +508 -71
- package/src/services/session-history-importer.ts +215 -51
- package/src/ui/app.js +1447 -55
- package/src/ui/index.html +318 -147
- package/src/ui/style.css +892 -0
- package/tests/bootstrap-organizer.test.ts +111 -0
- package/tests/consolidation-worker.test.ts +75 -0
- package/tests/ingest-interceptor.test.ts +38 -0
- package/tests/markdown-mirror.test.ts +85 -0
- package/tests/md-mirror.test.ts +50 -0
- package/tests/retriever-fallback-chain.test.ts +223 -0
- package/tests/retriever-strategy-scope.test.ts +97 -0
- package/tests/retriever.memu-adoption.test.ts +122 -0
- package/tests/sqlite-event-store-replication.test.ts +92 -0
- package/.claude/settings.local.json +0 -27
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201112328.json +0 -45
- package/.history/package_20260201113602.json +0 -45
- package/.history/package_20260201113713.json +0 -45
- package/.history/package_20260201114110.json +0 -45
- package/.history/package_20260201114632.json +0 -46
- package/.history/package_20260201133143.json +0 -45
- package/.history/package_20260201134319.json +0 -45
- package/.history/package_20260201134326.json +0 -45
- package/.history/package_20260201134334.json +0 -45
- package/.history/package_20260201134912.json +0 -45
- package/.history/package_20260201142928.json +0 -46
- package/.history/package_20260201192048.json +0 -47
- package/.history/package_20260202114053.json +0 -49
- package/.history/package_20260202121115.json +0 -49
- package/test_access.js +0 -49
|
@@ -10,14 +10,25 @@ import * as fs from 'fs';
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import * as readline from 'readline';
|
|
13
|
-
import {
|
|
13
|
+
import { randomUUID } from 'crypto';
|
|
14
|
+
import { MemoryService, registerSession } from './memory-service.js';
|
|
15
|
+
|
|
16
|
+
export type ProgressEvent =
|
|
17
|
+
| { phase: 'scan'; message: string }
|
|
18
|
+
| { phase: 'session-start'; sessionIndex: number; totalSessions: number; filePath: string }
|
|
19
|
+
| { phase: 'session-progress'; sessionIndex: number; messagesProcessed: number; imported: number; skipped: number }
|
|
20
|
+
| { phase: 'session-done'; sessionIndex: number; importedPrompts: number; importedResponses: number; skipped: number }
|
|
21
|
+
| { phase: 'embedding'; processed: number; total: number }
|
|
22
|
+
| { phase: 'done'; result: ImportResult };
|
|
14
23
|
|
|
15
24
|
export interface ImportOptions {
|
|
16
25
|
projectPath?: string;
|
|
17
26
|
sessionId?: string;
|
|
18
27
|
limit?: number;
|
|
19
28
|
skipExisting?: boolean;
|
|
29
|
+
force?: boolean;
|
|
20
30
|
verbose?: boolean;
|
|
31
|
+
onProgress?: (event: ProgressEvent) => void;
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
export interface ImportResult {
|
|
@@ -33,12 +44,62 @@ export interface ClaudeMessage {
|
|
|
33
44
|
type: string;
|
|
34
45
|
message?: {
|
|
35
46
|
role: string;
|
|
36
|
-
content: string | Array<{ type: string; text?: string }>;
|
|
47
|
+
content: string | Array<{ type: string; text?: string; name?: string; tool_use_id?: string }>;
|
|
37
48
|
};
|
|
38
49
|
sessionId?: string;
|
|
39
50
|
timestamp?: string;
|
|
40
51
|
}
|
|
41
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Classify a JSONL entry into a logical message type:
|
|
55
|
+
* - 'user_prompt': Real user input (string content or text blocks without tool_result)
|
|
56
|
+
* - 'tool_result': Tool execution result (user message with tool_result blocks)
|
|
57
|
+
* - 'agent_text': Assistant text response (text blocks)
|
|
58
|
+
* - 'tool_use': Assistant tool call (tool_use blocks)
|
|
59
|
+
* - 'thinking': Assistant thinking (thinking blocks)
|
|
60
|
+
* - 'skip': Everything else (progress, system, summary, etc.)
|
|
61
|
+
*/
|
|
62
|
+
function classifyEntry(entry: ClaudeMessage): 'user_prompt' | 'tool_result' | 'agent_text' | 'tool_use' | 'thinking' | 'skip' {
|
|
63
|
+
if (entry.type !== 'user' && entry.type !== 'assistant') {
|
|
64
|
+
return 'skip';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const content = entry.message?.content;
|
|
68
|
+
if (!content) return 'skip';
|
|
69
|
+
|
|
70
|
+
if (entry.type === 'user') {
|
|
71
|
+
// String content = real user input
|
|
72
|
+
if (typeof content === 'string') return 'user_prompt';
|
|
73
|
+
|
|
74
|
+
// Array content: check for tool_result blocks
|
|
75
|
+
if (Array.isArray(content)) {
|
|
76
|
+
const hasToolResult = content.some(b => b.type === 'tool_result');
|
|
77
|
+
if (hasToolResult) return 'tool_result';
|
|
78
|
+
|
|
79
|
+
// Text-only blocks from user = real user input
|
|
80
|
+
const hasText = content.some(b => b.type === 'text' && b.text);
|
|
81
|
+
if (hasText) return 'user_prompt';
|
|
82
|
+
}
|
|
83
|
+
return 'skip';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// assistant type
|
|
87
|
+
if (Array.isArray(content)) {
|
|
88
|
+
const hasToolUse = content.some(b => b.type === 'tool_use');
|
|
89
|
+
if (hasToolUse) return 'tool_use';
|
|
90
|
+
|
|
91
|
+
const hasText = content.some(b => b.type === 'text' && b.text);
|
|
92
|
+
if (hasText) return 'agent_text';
|
|
93
|
+
|
|
94
|
+
const hasThinking = content.some(b => b.type === 'thinking');
|
|
95
|
+
if (hasThinking) return 'thinking';
|
|
96
|
+
} else if (typeof content === 'string' && content.length > 0) {
|
|
97
|
+
return 'agent_text';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return 'skip';
|
|
101
|
+
}
|
|
102
|
+
|
|
42
103
|
export class SessionHistoryImporter {
|
|
43
104
|
private readonly memoryService: MemoryService;
|
|
44
105
|
private readonly claudeDir: string;
|
|
@@ -61,7 +122,10 @@ export class SessionHistoryImporter {
|
|
|
61
122
|
errors: []
|
|
62
123
|
};
|
|
63
124
|
|
|
125
|
+
const onProgress = options.onProgress;
|
|
126
|
+
|
|
64
127
|
// Find project directory
|
|
128
|
+
onProgress?.({ phase: 'scan', message: 'Scanning for session files...' });
|
|
65
129
|
const projectDir = await this.findProjectDir(projectPath);
|
|
66
130
|
if (!projectDir) {
|
|
67
131
|
result.errors.push(`Project directory not found for: ${projectPath}`);
|
|
@@ -71,19 +135,31 @@ export class SessionHistoryImporter {
|
|
|
71
135
|
// Find all session files
|
|
72
136
|
const sessionFiles = await this.findSessionFiles(projectDir);
|
|
73
137
|
result.totalSessions = sessionFiles.length;
|
|
138
|
+
onProgress?.({ phase: 'scan', message: `Found ${sessionFiles.length} sessions in ${path.basename(projectDir)}` });
|
|
74
139
|
|
|
75
140
|
if (options.verbose) {
|
|
76
141
|
console.log(`Found ${sessionFiles.length} session files in ${projectDir}`);
|
|
77
142
|
}
|
|
78
143
|
|
|
79
144
|
// Import each session
|
|
80
|
-
for (
|
|
145
|
+
for (let i = 0; i < sessionFiles.length; i++) {
|
|
146
|
+
const sessionFile = sessionFiles[i];
|
|
81
147
|
try {
|
|
82
|
-
|
|
148
|
+
onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: sessionFiles.length, filePath: sessionFile });
|
|
149
|
+
const sessionResult = await this.importSessionFile(sessionFile, {
|
|
150
|
+
...options,
|
|
151
|
+
_sessionIndex: i,
|
|
152
|
+
} as ImportOptions & { _sessionIndex: number });
|
|
83
153
|
result.totalMessages += sessionResult.totalMessages;
|
|
84
154
|
result.importedPrompts += sessionResult.importedPrompts;
|
|
85
155
|
result.importedResponses += sessionResult.importedResponses;
|
|
86
156
|
result.skippedDuplicates += sessionResult.skippedDuplicates;
|
|
157
|
+
onProgress?.({
|
|
158
|
+
phase: 'session-done', sessionIndex: i,
|
|
159
|
+
importedPrompts: sessionResult.importedPrompts,
|
|
160
|
+
importedResponses: sessionResult.importedResponses,
|
|
161
|
+
skipped: sessionResult.skippedDuplicates
|
|
162
|
+
});
|
|
87
163
|
} catch (error) {
|
|
88
164
|
result.errors.push(`Failed to import ${sessionFile}: ${error}`);
|
|
89
165
|
}
|
|
@@ -113,6 +189,14 @@ export class SessionHistoryImporter {
|
|
|
113
189
|
// Extract session ID from filename
|
|
114
190
|
const sessionId = path.basename(filePath, '.jsonl');
|
|
115
191
|
|
|
192
|
+
// Force reimport: delete existing events for this session
|
|
193
|
+
if (options.force) {
|
|
194
|
+
const deleted = await this.memoryService.deleteSessionEvents(sessionId);
|
|
195
|
+
if (options.verbose && deleted > 0) {
|
|
196
|
+
console.log(` Deleted ${deleted} existing events for session ${sessionId}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
116
200
|
// Start session in memory
|
|
117
201
|
await this.memoryService.startSession(sessionId, options.projectPath);
|
|
118
202
|
|
|
@@ -125,6 +209,51 @@ export class SessionHistoryImporter {
|
|
|
125
209
|
|
|
126
210
|
let lineCount = 0;
|
|
127
211
|
const limit = options.limit || Infinity;
|
|
212
|
+
const onProgress = options.onProgress;
|
|
213
|
+
const sessionIndex = (options as ImportOptions & { _sessionIndex?: number })._sessionIndex ?? 0;
|
|
214
|
+
let lastProgressAt = 0;
|
|
215
|
+
|
|
216
|
+
// Turn grouping with buffering:
|
|
217
|
+
// - Buffer assistant text blocks within a turn
|
|
218
|
+
// - On new user_prompt or EOF, flush buffer as a single merged agent_response
|
|
219
|
+
// - Filter out short transitional text (< 100 chars) like "Let me check..."
|
|
220
|
+
let currentTurnId: string | null = null;
|
|
221
|
+
let textBuffer: string[] = [];
|
|
222
|
+
let lastTimestamp: string | undefined;
|
|
223
|
+
|
|
224
|
+
// Flush buffered text as a single agent_response
|
|
225
|
+
const flushTextBuffer = async () => {
|
|
226
|
+
if (textBuffer.length === 0 || !currentTurnId) return;
|
|
227
|
+
|
|
228
|
+
// Filter: keep substantive text (>= 100 chars), discard short transitional phrases
|
|
229
|
+
const substantive = textBuffer.filter(t => t.length >= 100);
|
|
230
|
+
|
|
231
|
+
// If all filtered out, keep the longest block (there's always something meaningful)
|
|
232
|
+
const merged = substantive.length > 0
|
|
233
|
+
? substantive.join('\n\n')
|
|
234
|
+
: textBuffer.reduce((a, b) => a.length >= b.length ? a : b, '');
|
|
235
|
+
|
|
236
|
+
if (!merged) { textBuffer = []; return; }
|
|
237
|
+
|
|
238
|
+
// Truncate if very long
|
|
239
|
+
const truncated = merged.length > 10000
|
|
240
|
+
? merged.slice(0, 10000) + '...[truncated]'
|
|
241
|
+
: merged;
|
|
242
|
+
|
|
243
|
+
const appendResult = await this.memoryService.storeAgentResponse(
|
|
244
|
+
sessionId,
|
|
245
|
+
truncated,
|
|
246
|
+
{ importedFrom: filePath, originalTimestamp: lastTimestamp, turnId: currentTurnId }
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (appendResult.isDuplicate) {
|
|
250
|
+
result.skippedDuplicates++;
|
|
251
|
+
} else {
|
|
252
|
+
result.importedResponses++;
|
|
253
|
+
}
|
|
254
|
+
lineCount++;
|
|
255
|
+
textBuffer = [];
|
|
256
|
+
};
|
|
128
257
|
|
|
129
258
|
for await (const line of rl) {
|
|
130
259
|
if (lineCount >= limit) break;
|
|
@@ -133,43 +262,51 @@ export class SessionHistoryImporter {
|
|
|
133
262
|
const entry = JSON.parse(line) as ClaudeMessage;
|
|
134
263
|
result.totalMessages++;
|
|
135
264
|
|
|
136
|
-
|
|
137
|
-
|
|
265
|
+
const msgClass = classifyEntry(entry);
|
|
266
|
+
|
|
267
|
+
if (msgClass === 'user_prompt') {
|
|
268
|
+
// Flush previous turn's buffered responses before starting new turn
|
|
269
|
+
await flushTextBuffer();
|
|
270
|
+
|
|
138
271
|
const content = this.extractContent(entry);
|
|
139
272
|
if (!content) continue;
|
|
140
273
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
result.skippedDuplicates++;
|
|
150
|
-
} else {
|
|
151
|
-
result.importedPrompts++;
|
|
152
|
-
}
|
|
153
|
-
} else if (entry.type === 'assistant') {
|
|
154
|
-
// Truncate very long responses
|
|
155
|
-
const truncatedContent = content.length > 5000
|
|
156
|
-
? content.slice(0, 5000) + '...[truncated]'
|
|
157
|
-
: content;
|
|
158
|
-
|
|
159
|
-
const appendResult = await this.memoryService.storeAgentResponse(
|
|
160
|
-
sessionId,
|
|
161
|
-
truncatedContent,
|
|
162
|
-
{ importedFrom: filePath, originalTimestamp: entry.timestamp }
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
if (appendResult.isDuplicate) {
|
|
166
|
-
result.skippedDuplicates++;
|
|
167
|
-
} else {
|
|
168
|
-
result.importedResponses++;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
274
|
+
// New turn starts with each real user prompt
|
|
275
|
+
currentTurnId = randomUUID();
|
|
276
|
+
|
|
277
|
+
const appendResult = await this.memoryService.storeUserPrompt(
|
|
278
|
+
sessionId,
|
|
279
|
+
content,
|
|
280
|
+
{ importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
|
|
281
|
+
);
|
|
171
282
|
|
|
283
|
+
if (appendResult.isDuplicate) {
|
|
284
|
+
result.skippedDuplicates++;
|
|
285
|
+
} else {
|
|
286
|
+
result.importedPrompts++;
|
|
287
|
+
}
|
|
172
288
|
lineCount++;
|
|
289
|
+
} else if (msgClass === 'agent_text') {
|
|
290
|
+
// Buffer text instead of storing immediately
|
|
291
|
+
const content = this.extractContent(entry);
|
|
292
|
+
if (content) {
|
|
293
|
+
textBuffer.push(content);
|
|
294
|
+
lastTimestamp = entry.timestamp;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// tool_result, tool_use, thinking, skip → ignored
|
|
298
|
+
|
|
299
|
+
// Emit progress periodically
|
|
300
|
+
const now = Date.now();
|
|
301
|
+
if (now - lastProgressAt > 200) {
|
|
302
|
+
lastProgressAt = now;
|
|
303
|
+
onProgress?.({
|
|
304
|
+
phase: 'session-progress',
|
|
305
|
+
sessionIndex,
|
|
306
|
+
messagesProcessed: result.totalMessages,
|
|
307
|
+
imported: result.importedPrompts + result.importedResponses,
|
|
308
|
+
skipped: result.skippedDuplicates
|
|
309
|
+
});
|
|
173
310
|
}
|
|
174
311
|
} catch (parseError) {
|
|
175
312
|
// Skip malformed lines
|
|
@@ -177,9 +314,17 @@ export class SessionHistoryImporter {
|
|
|
177
314
|
}
|
|
178
315
|
}
|
|
179
316
|
|
|
317
|
+
// Flush any remaining buffered text from the last turn
|
|
318
|
+
await flushTextBuffer();
|
|
319
|
+
|
|
180
320
|
// End session
|
|
181
321
|
await this.memoryService.endSession(sessionId);
|
|
182
322
|
|
|
323
|
+
// Register session in registry so projects API can map hash → path
|
|
324
|
+
if (options.projectPath) {
|
|
325
|
+
registerSession(sessionId, options.projectPath);
|
|
326
|
+
}
|
|
327
|
+
|
|
183
328
|
if (options.verbose) {
|
|
184
329
|
console.log(`Imported ${result.importedPrompts} prompts, ${result.importedResponses} responses from ${filePath}`);
|
|
185
330
|
}
|
|
@@ -200,36 +345,55 @@ export class SessionHistoryImporter {
|
|
|
200
345
|
errors: []
|
|
201
346
|
};
|
|
202
347
|
|
|
348
|
+
const onProgress = options.onProgress;
|
|
349
|
+
|
|
203
350
|
const projectsDir = path.join(this.claudeDir, 'projects');
|
|
204
351
|
if (!fs.existsSync(projectsDir)) {
|
|
205
352
|
result.errors.push(`Projects directory not found: ${projectsDir}`);
|
|
206
353
|
return result;
|
|
207
354
|
}
|
|
208
355
|
|
|
209
|
-
// Find all project directories
|
|
356
|
+
// Find all project directories and session files
|
|
357
|
+
onProgress?.({ phase: 'scan', message: 'Scanning all projects...' });
|
|
210
358
|
const projectDirs = fs.readdirSync(projectsDir)
|
|
211
359
|
.map(name => path.join(projectsDir, name))
|
|
212
360
|
.filter(p => fs.statSync(p).isDirectory());
|
|
213
361
|
|
|
362
|
+
// Collect all session files across all projects
|
|
363
|
+
const allSessionFiles: string[] = [];
|
|
364
|
+
for (const projectDir of projectDirs) {
|
|
365
|
+
const sessionFiles = await this.findSessionFiles(projectDir);
|
|
366
|
+
allSessionFiles.push(...sessionFiles);
|
|
367
|
+
}
|
|
368
|
+
onProgress?.({ phase: 'scan', message: `Found ${allSessionFiles.length} sessions across ${projectDirs.length} projects` });
|
|
369
|
+
|
|
214
370
|
if (options.verbose) {
|
|
215
|
-
console.log(`Found ${projectDirs.length} project directories`);
|
|
371
|
+
console.log(`Found ${projectDirs.length} project directories, ${allSessionFiles.length} sessions`);
|
|
216
372
|
}
|
|
217
373
|
|
|
218
|
-
|
|
374
|
+
// Import all session files with progress tracking
|
|
375
|
+
for (let i = 0; i < allSessionFiles.length; i++) {
|
|
376
|
+
const sessionFile = allSessionFiles[i];
|
|
219
377
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
378
|
+
onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: allSessionFiles.length, filePath: sessionFile });
|
|
379
|
+
const sessionResult = await this.importSessionFile(sessionFile, {
|
|
380
|
+
...options,
|
|
381
|
+
_sessionIndex: i,
|
|
382
|
+
} as ImportOptions & { _sessionIndex: number });
|
|
383
|
+
result.totalSessions++;
|
|
384
|
+
result.totalMessages += sessionResult.totalMessages;
|
|
385
|
+
result.importedPrompts += sessionResult.importedPrompts;
|
|
386
|
+
result.importedResponses += sessionResult.importedResponses;
|
|
387
|
+
result.skippedDuplicates += sessionResult.skippedDuplicates;
|
|
388
|
+
result.errors.push(...sessionResult.errors);
|
|
389
|
+
onProgress?.({
|
|
390
|
+
phase: 'session-done', sessionIndex: i,
|
|
391
|
+
importedPrompts: sessionResult.importedPrompts,
|
|
392
|
+
importedResponses: sessionResult.importedResponses,
|
|
393
|
+
skipped: sessionResult.skippedDuplicates
|
|
394
|
+
});
|
|
231
395
|
} catch (error) {
|
|
232
|
-
result.errors.push(`Failed to process ${
|
|
396
|
+
result.errors.push(`Failed to process ${sessionFile}: ${error}`);
|
|
233
397
|
}
|
|
234
398
|
}
|
|
235
399
|
|