claude-memory-layer 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.
Files changed (127) hide show
  1. package/.claude-plugin/commands/memory-forget.md +42 -0
  2. package/.claude-plugin/commands/memory-history.md +34 -0
  3. package/.claude-plugin/commands/memory-import.md +56 -0
  4. package/.claude-plugin/commands/memory-list.md +37 -0
  5. package/.claude-plugin/commands/memory-search.md +36 -0
  6. package/.claude-plugin/commands/memory-stats.md +34 -0
  7. package/.claude-plugin/hooks.json +59 -0
  8. package/.claude-plugin/plugin.json +24 -0
  9. package/.history/package_20260201112328.json +45 -0
  10. package/.history/package_20260201113602.json +45 -0
  11. package/.history/package_20260201113713.json +45 -0
  12. package/.history/package_20260201114110.json +45 -0
  13. package/Memo.txt +558 -0
  14. package/README.md +520 -0
  15. package/context.md +636 -0
  16. package/dist/.claude-plugin/commands/memory-forget.md +42 -0
  17. package/dist/.claude-plugin/commands/memory-history.md +34 -0
  18. package/dist/.claude-plugin/commands/memory-import.md +56 -0
  19. package/dist/.claude-plugin/commands/memory-list.md +37 -0
  20. package/dist/.claude-plugin/commands/memory-search.md +36 -0
  21. package/dist/.claude-plugin/commands/memory-stats.md +34 -0
  22. package/dist/.claude-plugin/hooks.json +59 -0
  23. package/dist/.claude-plugin/plugin.json +24 -0
  24. package/dist/cli/index.js +3539 -0
  25. package/dist/cli/index.js.map +7 -0
  26. package/dist/core/index.js +4408 -0
  27. package/dist/core/index.js.map +7 -0
  28. package/dist/hooks/session-end.js +2971 -0
  29. package/dist/hooks/session-end.js.map +7 -0
  30. package/dist/hooks/session-start.js +2969 -0
  31. package/dist/hooks/session-start.js.map +7 -0
  32. package/dist/hooks/stop.js +3123 -0
  33. package/dist/hooks/stop.js.map +7 -0
  34. package/dist/hooks/user-prompt-submit.js +2960 -0
  35. package/dist/hooks/user-prompt-submit.js.map +7 -0
  36. package/dist/services/memory-service.js +2931 -0
  37. package/dist/services/memory-service.js.map +7 -0
  38. package/package.json +45 -0
  39. package/plan.md +1642 -0
  40. package/scripts/build.ts +102 -0
  41. package/spec.md +624 -0
  42. package/specs/citations-system/context.md +243 -0
  43. package/specs/citations-system/plan.md +495 -0
  44. package/specs/citations-system/spec.md +371 -0
  45. package/specs/endless-mode/context.md +305 -0
  46. package/specs/endless-mode/plan.md +620 -0
  47. package/specs/endless-mode/spec.md +455 -0
  48. package/specs/entity-edge-model/context.md +401 -0
  49. package/specs/entity-edge-model/plan.md +459 -0
  50. package/specs/entity-edge-model/spec.md +391 -0
  51. package/specs/evidence-aligner-v2/context.md +401 -0
  52. package/specs/evidence-aligner-v2/plan.md +303 -0
  53. package/specs/evidence-aligner-v2/spec.md +312 -0
  54. package/specs/mcp-desktop-integration/context.md +278 -0
  55. package/specs/mcp-desktop-integration/plan.md +550 -0
  56. package/specs/mcp-desktop-integration/spec.md +494 -0
  57. package/specs/post-tool-use-hook/context.md +319 -0
  58. package/specs/post-tool-use-hook/plan.md +469 -0
  59. package/specs/post-tool-use-hook/spec.md +364 -0
  60. package/specs/private-tags/context.md +288 -0
  61. package/specs/private-tags/plan.md +412 -0
  62. package/specs/private-tags/spec.md +345 -0
  63. package/specs/progressive-disclosure/context.md +346 -0
  64. package/specs/progressive-disclosure/plan.md +663 -0
  65. package/specs/progressive-disclosure/spec.md +415 -0
  66. package/specs/task-entity-system/context.md +297 -0
  67. package/specs/task-entity-system/plan.md +301 -0
  68. package/specs/task-entity-system/spec.md +314 -0
  69. package/specs/vector-outbox-v2/context.md +470 -0
  70. package/specs/vector-outbox-v2/plan.md +562 -0
  71. package/specs/vector-outbox-v2/spec.md +466 -0
  72. package/specs/web-viewer-ui/context.md +384 -0
  73. package/specs/web-viewer-ui/plan.md +797 -0
  74. package/specs/web-viewer-ui/spec.md +516 -0
  75. package/src/cli/index.ts +570 -0
  76. package/src/core/canonical-key.ts +186 -0
  77. package/src/core/citation-generator.ts +63 -0
  78. package/src/core/consolidated-store.ts +279 -0
  79. package/src/core/consolidation-worker.ts +384 -0
  80. package/src/core/context-formatter.ts +276 -0
  81. package/src/core/continuity-manager.ts +336 -0
  82. package/src/core/edge-repo.ts +324 -0
  83. package/src/core/embedder.ts +124 -0
  84. package/src/core/entity-repo.ts +342 -0
  85. package/src/core/event-store.ts +672 -0
  86. package/src/core/evidence-aligner.ts +635 -0
  87. package/src/core/graduation.ts +365 -0
  88. package/src/core/index.ts +32 -0
  89. package/src/core/matcher.ts +210 -0
  90. package/src/core/metadata-extractor.ts +203 -0
  91. package/src/core/privacy/filter.ts +179 -0
  92. package/src/core/privacy/index.ts +20 -0
  93. package/src/core/privacy/tag-parser.ts +145 -0
  94. package/src/core/progressive-retriever.ts +415 -0
  95. package/src/core/retriever.ts +235 -0
  96. package/src/core/task/blocker-resolver.ts +325 -0
  97. package/src/core/task/index.ts +9 -0
  98. package/src/core/task/task-matcher.ts +238 -0
  99. package/src/core/task/task-projector.ts +345 -0
  100. package/src/core/task/task-resolver.ts +414 -0
  101. package/src/core/types.ts +841 -0
  102. package/src/core/vector-outbox.ts +295 -0
  103. package/src/core/vector-store.ts +182 -0
  104. package/src/core/vector-worker.ts +488 -0
  105. package/src/core/working-set-store.ts +244 -0
  106. package/src/hooks/post-tool-use.ts +127 -0
  107. package/src/hooks/session-end.ts +78 -0
  108. package/src/hooks/session-start.ts +57 -0
  109. package/src/hooks/stop.ts +78 -0
  110. package/src/hooks/user-prompt-submit.ts +54 -0
  111. package/src/mcp/handlers.ts +212 -0
  112. package/src/mcp/index.ts +47 -0
  113. package/src/mcp/tools.ts +78 -0
  114. package/src/server/api/citations.ts +101 -0
  115. package/src/server/api/events.ts +101 -0
  116. package/src/server/api/index.ts +18 -0
  117. package/src/server/api/search.ts +98 -0
  118. package/src/server/api/sessions.ts +111 -0
  119. package/src/server/api/stats.ts +97 -0
  120. package/src/server/index.ts +91 -0
  121. package/src/services/memory-service.ts +626 -0
  122. package/src/services/session-history-importer.ts +367 -0
  123. package/tests/canonical-key.test.ts +101 -0
  124. package/tests/evidence-aligner.test.ts +152 -0
  125. package/tests/matcher.test.ts +112 -0
  126. package/tsconfig.json +24 -0
  127. package/vitest.config.ts +15 -0
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Consolidation Worker
3
+ * Periodically consolidates working set into long-term memory
4
+ * Biomimetic: Simulates memory consolidation during sleep/idle periods
5
+ */
6
+
7
+ import type {
8
+ EndlessModeConfig,
9
+ MemoryEvent,
10
+ EventGroup,
11
+ WorkingSet
12
+ } from './types.js';
13
+ import { WorkingSetStore } from './working-set-store.js';
14
+ import { ConsolidatedStore } from './consolidated-store.js';
15
+
16
+ export class ConsolidationWorker {
17
+ private running = false;
18
+ private timeout: NodeJS.Timeout | null = null;
19
+ private lastActivity: Date = new Date();
20
+
21
+ constructor(
22
+ private workingSetStore: WorkingSetStore,
23
+ private consolidatedStore: ConsolidatedStore,
24
+ private config: EndlessModeConfig
25
+ ) {}
26
+
27
+ /**
28
+ * Start the consolidation worker
29
+ */
30
+ start(): void {
31
+ if (this.running) return;
32
+ this.running = true;
33
+ this.scheduleNext();
34
+ }
35
+
36
+ /**
37
+ * Stop the consolidation worker
38
+ */
39
+ stop(): void {
40
+ this.running = false;
41
+ if (this.timeout) {
42
+ clearTimeout(this.timeout);
43
+ this.timeout = null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Record activity (resets idle timer)
49
+ */
50
+ recordActivity(): void {
51
+ this.lastActivity = new Date();
52
+ }
53
+
54
+ /**
55
+ * Check if currently running
56
+ */
57
+ isRunning(): boolean {
58
+ return this.running;
59
+ }
60
+
61
+ /**
62
+ * Force a consolidation run (manual trigger)
63
+ */
64
+ async forceRun(): Promise<number> {
65
+ return await this.consolidate();
66
+ }
67
+
68
+ /**
69
+ * Schedule the next consolidation check
70
+ */
71
+ private scheduleNext(): void {
72
+ if (!this.running) return;
73
+
74
+ this.timeout = setTimeout(
75
+ () => this.run(),
76
+ this.config.consolidation.triggerIntervalMs
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Run consolidation check
82
+ */
83
+ private async run(): Promise<void> {
84
+ if (!this.running) return;
85
+
86
+ try {
87
+ await this.checkAndConsolidate();
88
+ } catch (error) {
89
+ console.error('Consolidation error:', error);
90
+ }
91
+
92
+ this.scheduleNext();
93
+ }
94
+
95
+ /**
96
+ * Check conditions and consolidate if needed
97
+ */
98
+ private async checkAndConsolidate(): Promise<void> {
99
+ const workingSet = await this.workingSetStore.get();
100
+
101
+ if (!this.shouldConsolidate(workingSet)) {
102
+ return;
103
+ }
104
+
105
+ await this.consolidate();
106
+ }
107
+
108
+ /**
109
+ * Perform consolidation
110
+ */
111
+ private async consolidate(): Promise<number> {
112
+ const workingSet = await this.workingSetStore.get();
113
+
114
+ if (workingSet.recentEvents.length < 3) {
115
+ return 0; // Not enough events to consolidate
116
+ }
117
+
118
+ // Group events by topic
119
+ const groups = this.groupByTopic(workingSet.recentEvents);
120
+ let consolidatedCount = 0;
121
+
122
+ for (const group of groups) {
123
+ // Require minimum 3 events per group
124
+ if (group.events.length < 3) continue;
125
+
126
+ // Check if already consolidated
127
+ const eventIds = group.events.map(e => e.id);
128
+ const alreadyConsolidated = await this.consolidatedStore.isAlreadyConsolidated(eventIds);
129
+ if (alreadyConsolidated) continue;
130
+
131
+ // Generate summary
132
+ const summary = await this.summarize(group);
133
+
134
+ // Create consolidated memory
135
+ await this.consolidatedStore.create({
136
+ summary,
137
+ topics: group.topics,
138
+ sourceEvents: eventIds,
139
+ confidence: this.calculateConfidence(group)
140
+ });
141
+
142
+ consolidatedCount++;
143
+ }
144
+
145
+ // Prune consolidated events from working set
146
+ if (consolidatedCount > 0) {
147
+ const consolidatedEventIds = groups
148
+ .filter(g => g.events.length >= 3)
149
+ .flatMap(g => g.events.map(e => e.id));
150
+
151
+ // Only prune old events (keep recent for context)
152
+ const oldEventIds = consolidatedEventIds.filter(id => {
153
+ const event = workingSet.recentEvents.find(e => e.id === id);
154
+ if (!event) return false;
155
+ const ageHours = (Date.now() - event.timestamp.getTime()) / (1000 * 60 * 60);
156
+ return ageHours > this.config.workingSet.timeWindowHours / 2;
157
+ });
158
+
159
+ if (oldEventIds.length > 0) {
160
+ await this.workingSetStore.prune(oldEventIds);
161
+ }
162
+ }
163
+
164
+ return consolidatedCount;
165
+ }
166
+
167
+ /**
168
+ * Check if consolidation should run
169
+ */
170
+ private shouldConsolidate(workingSet: WorkingSet): boolean {
171
+ // Check event count trigger
172
+ if (workingSet.recentEvents.length >= this.config.consolidation.triggerEventCount) {
173
+ return true;
174
+ }
175
+
176
+ // Check idle time trigger
177
+ const idleTime = Date.now() - this.lastActivity.getTime();
178
+ if (idleTime >= this.config.consolidation.triggerIdleMs) {
179
+ return true;
180
+ }
181
+
182
+ return false;
183
+ }
184
+
185
+ /**
186
+ * Group events by topic using simple keyword extraction
187
+ */
188
+ private groupByTopic(events: MemoryEvent[]): EventGroup[] {
189
+ const groups = new Map<string, EventGroup>();
190
+
191
+ for (const event of events) {
192
+ const topics = this.extractTopics(event.content);
193
+
194
+ for (const topic of topics) {
195
+ if (!groups.has(topic)) {
196
+ groups.set(topic, { topics: [topic], events: [] });
197
+ }
198
+ const group = groups.get(topic)!;
199
+ if (!group.events.find(e => e.id === event.id)) {
200
+ group.events.push(event);
201
+ }
202
+ }
203
+ }
204
+
205
+ // Merge groups with overlapping events
206
+ const mergedGroups = this.mergeOverlappingGroups(Array.from(groups.values()));
207
+
208
+ return mergedGroups;
209
+ }
210
+
211
+ /**
212
+ * Extract topics from content using simple keyword extraction
213
+ */
214
+ private extractTopics(content: string): string[] {
215
+ const topics: string[] = [];
216
+
217
+ // Extract code-related keywords
218
+ const codePatterns = [
219
+ /\b(function|class|interface|type|const|let|var)\s+(\w+)/gi,
220
+ /\b(import|export)\s+.*?from\s+['"]([^'"]+)['"]/gi,
221
+ /\bfile[:\s]+([^\s,]+)/gi
222
+ ];
223
+
224
+ for (const pattern of codePatterns) {
225
+ let match;
226
+ while ((match = pattern.exec(content)) !== null) {
227
+ const keyword = match[2] || match[1];
228
+ if (keyword && keyword.length > 2) {
229
+ topics.push(keyword.toLowerCase());
230
+ }
231
+ }
232
+ }
233
+
234
+ // Extract common programming terms
235
+ const commonTerms = [
236
+ 'bug', 'fix', 'error', 'issue', 'feature',
237
+ 'test', 'refactor', 'implement', 'add', 'remove',
238
+ 'update', 'change', 'modify', 'create', 'delete'
239
+ ];
240
+
241
+ const contentLower = content.toLowerCase();
242
+ for (const term of commonTerms) {
243
+ if (contentLower.includes(term)) {
244
+ topics.push(term);
245
+ }
246
+ }
247
+
248
+ return [...new Set(topics)].slice(0, 5); // Limit to 5 topics
249
+ }
250
+
251
+ /**
252
+ * Merge groups that have significant event overlap
253
+ */
254
+ private mergeOverlappingGroups(groups: EventGroup[]): EventGroup[] {
255
+ const merged: EventGroup[] = [];
256
+
257
+ for (const group of groups) {
258
+ let foundMerge = false;
259
+
260
+ for (const existing of merged) {
261
+ const overlap = group.events.filter(e =>
262
+ existing.events.some(ex => ex.id === e.id)
263
+ );
264
+
265
+ // If > 50% overlap, merge
266
+ if (overlap.length > group.events.length / 2) {
267
+ existing.topics = [...new Set([...existing.topics, ...group.topics])];
268
+ for (const event of group.events) {
269
+ if (!existing.events.find(e => e.id === event.id)) {
270
+ existing.events.push(event);
271
+ }
272
+ }
273
+ foundMerge = true;
274
+ break;
275
+ }
276
+ }
277
+
278
+ if (!foundMerge) {
279
+ merged.push(group);
280
+ }
281
+ }
282
+
283
+ return merged;
284
+ }
285
+
286
+ /**
287
+ * Generate summary for a group of events
288
+ * Rule-based extraction (no LLM by default)
289
+ */
290
+ private async summarize(group: EventGroup): Promise<string> {
291
+ if (this.config.consolidation.useLLMSummarization) {
292
+ // Future: LLM-based summarization
293
+ return this.ruleBasedSummary(group);
294
+ }
295
+
296
+ return this.ruleBasedSummary(group);
297
+ }
298
+
299
+ /**
300
+ * Rule-based summary generation
301
+ */
302
+ private ruleBasedSummary(group: EventGroup): string {
303
+ const keyPoints: string[] = [];
304
+
305
+ for (const event of group.events.slice(0, 10)) {
306
+ const keyPoint = this.extractKeyPoint(event.content);
307
+ if (keyPoint) {
308
+ keyPoints.push(keyPoint);
309
+ }
310
+ }
311
+
312
+ const topicsStr = group.topics.slice(0, 3).join(', ');
313
+ const summary = [
314
+ `Topics: ${topicsStr}`,
315
+ '',
316
+ 'Key points:',
317
+ ...keyPoints.map(kp => `- ${kp}`)
318
+ ].join('\n');
319
+
320
+ return summary;
321
+ }
322
+
323
+ /**
324
+ * Extract key point from content
325
+ */
326
+ private extractKeyPoint(content: string): string | null {
327
+ // Get first meaningful sentence
328
+ const sentences = content.split(/[.!?\n]+/).filter(s => s.trim().length > 10);
329
+ if (sentences.length === 0) return null;
330
+
331
+ const firstSentence = sentences[0].trim();
332
+
333
+ // Truncate if too long
334
+ if (firstSentence.length > 100) {
335
+ return firstSentence.slice(0, 100) + '...';
336
+ }
337
+
338
+ return firstSentence;
339
+ }
340
+
341
+ /**
342
+ * Calculate confidence score for a group
343
+ */
344
+ private calculateConfidence(group: EventGroup): number {
345
+ // Factor 1: Event count (more events = higher confidence)
346
+ const eventScore = Math.min(group.events.length / 10, 1);
347
+
348
+ // Factor 2: Time proximity (events closer together = higher confidence)
349
+ const timeScore = this.calculateTimeProximity(group.events);
350
+
351
+ // Factor 3: Topic consistency (fewer topics per event = higher confidence)
352
+ const topicScore = Math.min(3 / group.topics.length, 1);
353
+
354
+ return (eventScore * 0.4 + timeScore * 0.4 + topicScore * 0.2);
355
+ }
356
+
357
+ /**
358
+ * Calculate time proximity score
359
+ */
360
+ private calculateTimeProximity(events: MemoryEvent[]): number {
361
+ if (events.length < 2) return 1;
362
+
363
+ const timestamps = events.map(e => e.timestamp.getTime()).sort((a, b) => a - b);
364
+ const timeSpan = timestamps[timestamps.length - 1] - timestamps[0];
365
+
366
+ // Score based on average time between events
367
+ const avgGap = timeSpan / (events.length - 1);
368
+ const hourInMs = 60 * 60 * 1000;
369
+
370
+ // Within 1 hour average = score 1, 24 hours = score 0.5, etc.
371
+ return Math.max(0, 1 - (avgGap / (24 * hourInMs)));
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Create a Consolidation Worker instance
377
+ */
378
+ export function createConsolidationWorker(
379
+ workingSetStore: WorkingSetStore,
380
+ consolidatedStore: ConsolidatedStore,
381
+ config: EndlessModeConfig
382
+ ): ConsolidationWorker {
383
+ return new ConsolidationWorker(workingSetStore, consolidatedStore, config);
384
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Context Formatter
3
+ * Formats progressive search results for Claude context injection
4
+ */
5
+
6
+ import type {
7
+ ProgressiveSearchResult,
8
+ SearchIndexItem,
9
+ TimelineItem,
10
+ FullDetail,
11
+ CitedSearchResult
12
+ } from './types.js';
13
+ import { formatCitationId } from './citation-generator.js';
14
+
15
+ export interface FormatOptions {
16
+ format?: 'inline' | 'footnote' | 'reference';
17
+ showTokens?: boolean;
18
+ maxWidth?: number;
19
+ }
20
+
21
+ export class ContextFormatter {
22
+ /**
23
+ * Format progressive search result for Claude context
24
+ */
25
+ formatProgressiveResult(
26
+ result: ProgressiveSearchResult,
27
+ options?: FormatOptions
28
+ ): string {
29
+ const parts: string[] = [];
30
+
31
+ // Layer 1: Always included (index)
32
+ parts.push(this.formatLayer1(result.index));
33
+
34
+ // Layer 2: Timeline (if expanded)
35
+ if (result.timeline && result.timeline.length > 0) {
36
+ parts.push(this.formatLayer2(result.timeline));
37
+ }
38
+
39
+ // Layer 3: Details (if expanded)
40
+ if (result.details && result.details.length > 0) {
41
+ parts.push(this.formatLayer3(result.details, options));
42
+ }
43
+
44
+ // Meta information
45
+ if (options?.showTokens !== false) {
46
+ parts.push(this.formatMeta(result.meta));
47
+ }
48
+
49
+ return parts.join('\n\n');
50
+ }
51
+
52
+ /**
53
+ * Format Layer 1: Search Index
54
+ */
55
+ private formatLayer1(items: SearchIndexItem[]): string {
56
+ if (items.length === 0) {
57
+ return '## Related Memories\n\nNo relevant memories found.';
58
+ }
59
+
60
+ const header = `## Related Memories (${items.length} matches)\n`;
61
+ const rows = items.map((item, i) => {
62
+ const date = item.timestamp.toISOString().split('T')[0];
63
+ return `${i + 1}. **[${item.id.slice(0, 8)}]** ${item.summary} _(${date}, score: ${item.score.toFixed(2)})_`;
64
+ }).join('\n');
65
+
66
+ return header + rows;
67
+ }
68
+
69
+ /**
70
+ * Format Layer 2: Timeline
71
+ */
72
+ private formatLayer2(items: TimelineItem[]): string {
73
+ const header = '## Timeline Context\n';
74
+ const timeline = items.map(item => {
75
+ const marker = item.isTarget ? '**→**' : ' ';
76
+ const time = item.timestamp.toLocaleTimeString('en-US', {
77
+ hour: '2-digit',
78
+ minute: '2-digit'
79
+ });
80
+ const typeIcon = this.getTypeIcon(item.type);
81
+ return `${marker} ${time} ${typeIcon} ${item.preview}`;
82
+ }).join('\n');
83
+
84
+ return header + timeline;
85
+ }
86
+
87
+ /**
88
+ * Format Layer 3: Full Details
89
+ */
90
+ private formatLayer3(items: FullDetail[], options?: FormatOptions): string {
91
+ const format = options?.format ?? 'inline';
92
+
93
+ switch (format) {
94
+ case 'inline':
95
+ return this.formatDetailsInline(items);
96
+ case 'footnote':
97
+ return this.formatDetailsFootnote(items);
98
+ case 'reference':
99
+ return this.formatDetailsReference(items);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Inline format for details
105
+ */
106
+ private formatDetailsInline(items: FullDetail[]): string {
107
+ return items.map(item => {
108
+ const date = item.timestamp.toLocaleDateString();
109
+ const session = item.sessionId.slice(0, 8);
110
+ const citation = item.citationId ? formatCitationId(item.citationId) : '';
111
+
112
+ const header = `## Detail: ${item.id.slice(0, 8)}`;
113
+ const meta = `_${item.type} | Session: ${session} | ${date}_`;
114
+ const content = item.content;
115
+ const footer = citation ? `\n${citation}` : '';
116
+
117
+ return [header, meta, '', content, footer].join('\n');
118
+ }).join('\n\n---\n\n');
119
+ }
120
+
121
+ /**
122
+ * Footnote format for details
123
+ */
124
+ private formatDetailsFootnote(items: FullDetail[]): string {
125
+ const content = items.map((item, i) => {
126
+ return `${item.content} [${i + 1}]`;
127
+ }).join('\n\n');
128
+
129
+ const footnotes = items.map((item, i) => {
130
+ const citation = item.citationId ? formatCitationId(item.citationId) : `[${item.id.slice(0, 8)}]`;
131
+ const date = item.timestamp.toLocaleDateString();
132
+ return `[${i + 1}] ${citation} - ${date}`;
133
+ }).join('\n');
134
+
135
+ return `${content}\n\n---\n**References:**\n${footnotes}`;
136
+ }
137
+
138
+ /**
139
+ * Reference format for details
140
+ */
141
+ private formatDetailsReference(items: FullDetail[]): string {
142
+ const content = items.map(item => {
143
+ return `### ${item.type}\n${item.content}`;
144
+ }).join('\n\n');
145
+
146
+ const references = items.map(item => {
147
+ const citation = item.citationId ? formatCitationId(item.citationId) : `[${item.id.slice(0, 8)}]`;
148
+ const date = item.timestamp.toLocaleDateString();
149
+ return `- ${citation} Session ${item.sessionId.slice(0, 8)}, ${date}`;
150
+ }).join('\n');
151
+
152
+ return `## Content\n\n${content}\n\n## References\n${references}`;
153
+ }
154
+
155
+ /**
156
+ * Format meta information
157
+ */
158
+ private formatMeta(meta: ProgressiveSearchResult['meta']): string {
159
+ const parts: string[] = [];
160
+
161
+ if (meta.expansionReason) {
162
+ const reasonText = this.getExpansionReasonText(meta.expansionReason);
163
+ parts.push(`_${reasonText}_`);
164
+ }
165
+
166
+ parts.push(`_~${meta.estimatedTokens} tokens | ${meta.expandedCount} expanded_`);
167
+
168
+ return parts.join(' | ');
169
+ }
170
+
171
+ /**
172
+ * Get icon for event type
173
+ */
174
+ private getTypeIcon(type: string): string {
175
+ switch (type) {
176
+ case 'user_prompt':
177
+ return '👤';
178
+ case 'agent_response':
179
+ return '🤖';
180
+ case 'session_summary':
181
+ return '📋';
182
+ case 'tool_observation':
183
+ return '🔧';
184
+ default:
185
+ return '📄';
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Get human-readable expansion reason
191
+ */
192
+ private getExpansionReasonText(reason: string): string {
193
+ switch (reason) {
194
+ case 'high_confidence_single':
195
+ return 'High confidence match - auto-expanded';
196
+ case 'clear_winner':
197
+ return 'Clear best match found';
198
+ case 'ambiguous_multiple_high':
199
+ return 'Multiple relevant results - showing timeline';
200
+ case 'low_confidence':
201
+ return 'No high confidence matches';
202
+ case 'no_results':
203
+ return 'No matches found';
204
+ default:
205
+ return reason;
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Format context with citations (for cited search results)
212
+ */
213
+ export function formatContextWithCitations(
214
+ results: CitedSearchResult[],
215
+ options?: FormatOptions
216
+ ): string {
217
+ const format = options?.format ?? 'inline';
218
+
219
+ switch (format) {
220
+ case 'inline':
221
+ return formatCitedInline(results);
222
+ case 'footnote':
223
+ return formatCitedFootnote(results);
224
+ case 'reference':
225
+ return formatCitedReference(results);
226
+ }
227
+ }
228
+
229
+ function formatCitedInline(results: CitedSearchResult[]): string {
230
+ return results.map(r => {
231
+ const date = r.event.timestamp.toLocaleDateString();
232
+ const session = r.event.sessionId.slice(0, 8);
233
+ const citation = formatCitationId(r.citation.citationId);
234
+
235
+ return [
236
+ `> ${r.event.content}`,
237
+ `>`,
238
+ `> ${citation} - ${date}, Session ${session}`
239
+ ].join('\n');
240
+ }).join('\n\n---\n\n');
241
+ }
242
+
243
+ function formatCitedFootnote(results: CitedSearchResult[]): string {
244
+ const content = results.map((r, i) => {
245
+ return `${r.event.content} [${i + 1}]`;
246
+ }).join('\n\n');
247
+
248
+ const footnotes = results.map((r, i) => {
249
+ const citation = formatCitationId(r.citation.citationId);
250
+ const date = r.event.timestamp.toLocaleDateString();
251
+ return `[${i + 1}] ${citation} - ${date}`;
252
+ }).join('\n');
253
+
254
+ return `${content}\n\n---\n**References:**\n${footnotes}`;
255
+ }
256
+
257
+ function formatCitedReference(results: CitedSearchResult[]): string {
258
+ const content = results.map(r => {
259
+ return `### ${r.event.eventType}\n${r.event.content}`;
260
+ }).join('\n\n');
261
+
262
+ const references = results.map(r => {
263
+ const citation = formatCitationId(r.citation.citationId);
264
+ const date = r.event.timestamp.toLocaleDateString();
265
+ return `- ${citation} Session ${r.event.sessionId.slice(0, 8)}, ${date}`;
266
+ }).join('\n');
267
+
268
+ return `## Content\n\n${content}\n\n## References\n${references}`;
269
+ }
270
+
271
+ /**
272
+ * Create a context formatter instance
273
+ */
274
+ export function createContextFormatter(): ContextFormatter {
275
+ return new ContextFormatter();
276
+ }