sidekick-shared 0.13.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.
Files changed (126) hide show
  1. package/README.md +92 -0
  2. package/dist/aggregation/EventAggregator.d.ts +172 -0
  3. package/dist/aggregation/EventAggregator.js +1443 -0
  4. package/dist/aggregation/FrequencyTracker.d.ts +42 -0
  5. package/dist/aggregation/FrequencyTracker.js +73 -0
  6. package/dist/aggregation/HeatmapTracker.d.ts +40 -0
  7. package/dist/aggregation/HeatmapTracker.js +93 -0
  8. package/dist/aggregation/PatternExtractor.d.ts +51 -0
  9. package/dist/aggregation/PatternExtractor.js +171 -0
  10. package/dist/aggregation/snapshot.d.ts +64 -0
  11. package/dist/aggregation/snapshot.js +151 -0
  12. package/dist/aggregation/types.d.ts +121 -0
  13. package/dist/aggregation/types.js +6 -0
  14. package/dist/context/composer.d.ts +31 -0
  15. package/dist/context/composer.js +72 -0
  16. package/dist/credentials.d.ts +23 -0
  17. package/dist/credentials.js +96 -0
  18. package/dist/formatters/eventHighlighter.d.ts +30 -0
  19. package/dist/formatters/eventHighlighter.js +217 -0
  20. package/dist/formatters/noiseClassifier.d.ts +73 -0
  21. package/dist/formatters/noiseClassifier.js +226 -0
  22. package/dist/formatters/sessionDump.d.ts +38 -0
  23. package/dist/formatters/sessionDump.js +313 -0
  24. package/dist/formatters/toolSummary.d.ts +23 -0
  25. package/dist/formatters/toolSummary.js +230 -0
  26. package/dist/index.d.ts +85 -0
  27. package/dist/index.js +182 -0
  28. package/dist/parsers/changelogParser.d.ts +25 -0
  29. package/dist/parsers/changelogParser.js +74 -0
  30. package/dist/parsers/codexParser.d.ts +76 -0
  31. package/dist/parsers/codexParser.js +653 -0
  32. package/dist/parsers/debugLogParser.d.ts +63 -0
  33. package/dist/parsers/debugLogParser.js +164 -0
  34. package/dist/parsers/jsonl.d.ts +45 -0
  35. package/dist/parsers/jsonl.js +57 -0
  36. package/dist/parsers/openCodeParser.d.ts +64 -0
  37. package/dist/parsers/openCodeParser.js +581 -0
  38. package/dist/parsers/planExtractor.d.ts +63 -0
  39. package/dist/parsers/planExtractor.js +330 -0
  40. package/dist/parsers/sessionActivityDetector.d.ts +31 -0
  41. package/dist/parsers/sessionActivityDetector.js +184 -0
  42. package/dist/parsers/sessionPathResolver.d.ts +230 -0
  43. package/dist/parsers/sessionPathResolver.js +753 -0
  44. package/dist/parsers/subagentScanner.d.ts +43 -0
  45. package/dist/parsers/subagentScanner.js +366 -0
  46. package/dist/parsers/subagentTraceParser.d.ts +58 -0
  47. package/dist/parsers/subagentTraceParser.js +346 -0
  48. package/dist/paths.d.ts +38 -0
  49. package/dist/paths.js +107 -0
  50. package/dist/phrases.d.ts +52 -0
  51. package/dist/phrases.js +1333 -0
  52. package/dist/providers/claudeCode.d.ts +48 -0
  53. package/dist/providers/claudeCode.js +465 -0
  54. package/dist/providers/codex.d.ts +57 -0
  55. package/dist/providers/codex.js +944 -0
  56. package/dist/providers/codexDatabase.d.ts +37 -0
  57. package/dist/providers/codexDatabase.js +148 -0
  58. package/dist/providers/detect.d.ts +16 -0
  59. package/dist/providers/detect.js +162 -0
  60. package/dist/providers/openCode.d.ts +70 -0
  61. package/dist/providers/openCode.js +1524 -0
  62. package/dist/providers/openCodeDatabase.d.ts +87 -0
  63. package/dist/providers/openCodeDatabase.js +232 -0
  64. package/dist/providers/types.d.ts +154 -0
  65. package/dist/providers/types.js +12 -0
  66. package/dist/quota.d.ts +34 -0
  67. package/dist/quota.js +80 -0
  68. package/dist/readers/decisions.d.ts +10 -0
  69. package/dist/readers/decisions.js +27 -0
  70. package/dist/readers/handoff.d.ts +4 -0
  71. package/dist/readers/handoff.js +51 -0
  72. package/dist/readers/helpers.d.ts +7 -0
  73. package/dist/readers/helpers.js +52 -0
  74. package/dist/readers/history.d.ts +5 -0
  75. package/dist/readers/history.js +12 -0
  76. package/dist/readers/notes.d.ts +10 -0
  77. package/dist/readers/notes.js +46 -0
  78. package/dist/readers/plans.d.ts +35 -0
  79. package/dist/readers/plans.js +247 -0
  80. package/dist/readers/tasks.d.ts +8 -0
  81. package/dist/readers/tasks.js +22 -0
  82. package/dist/report/htmlHelpers.d.ts +18 -0
  83. package/dist/report/htmlHelpers.js +166 -0
  84. package/dist/report/htmlReportGenerator.d.ts +11 -0
  85. package/dist/report/htmlReportGenerator.js +650 -0
  86. package/dist/report/index.d.ts +8 -0
  87. package/dist/report/index.js +16 -0
  88. package/dist/report/logo.d.ts +2 -0
  89. package/dist/report/logo.js +5 -0
  90. package/dist/report/openBrowser.d.ts +5 -0
  91. package/dist/report/openBrowser.js +22 -0
  92. package/dist/report/transcriptParser.d.ts +12 -0
  93. package/dist/report/transcriptParser.js +177 -0
  94. package/dist/report/types.d.ts +43 -0
  95. package/dist/report/types.js +5 -0
  96. package/dist/search/advancedFilter.d.ts +62 -0
  97. package/dist/search/advancedFilter.js +201 -0
  98. package/dist/search/sessionSearch.d.ts +16 -0
  99. package/dist/search/sessionSearch.js +93 -0
  100. package/dist/types/codex.d.ts +276 -0
  101. package/dist/types/codex.js +14 -0
  102. package/dist/types/decisionLog.d.ts +23 -0
  103. package/dist/types/decisionLog.js +8 -0
  104. package/dist/types/historicalData.d.ts +74 -0
  105. package/dist/types/historicalData.js +17 -0
  106. package/dist/types/knowledgeNote.d.ts +40 -0
  107. package/dist/types/knowledgeNote.js +18 -0
  108. package/dist/types/opencode.d.ts +268 -0
  109. package/dist/types/opencode.js +13 -0
  110. package/dist/types/plan.d.ts +49 -0
  111. package/dist/types/plan.js +10 -0
  112. package/dist/types/sessionEvent.d.ts +562 -0
  113. package/dist/types/sessionEvent.js +11 -0
  114. package/dist/types/taskPersistence.d.ts +33 -0
  115. package/dist/types/taskPersistence.js +16 -0
  116. package/dist/watchers/eventBridge.d.ts +19 -0
  117. package/dist/watchers/eventBridge.js +162 -0
  118. package/dist/watchers/factory.d.ts +15 -0
  119. package/dist/watchers/factory.js +85 -0
  120. package/dist/watchers/jsonlWatcher.d.ts +30 -0
  121. package/dist/watchers/jsonlWatcher.js +444 -0
  122. package/dist/watchers/sqliteWatcher.d.ts +30 -0
  123. package/dist/watchers/sqliteWatcher.js +278 -0
  124. package/dist/watchers/types.d.ts +60 -0
  125. package/dist/watchers/types.js +5 -0
  126. package/package.json +31 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Generic frequency counter with LRU eviction.
3
+ *
4
+ * Tracks word/key frequencies with count, firstSeen, lastSeen metadata.
5
+ * Used for tool name frequency, event type frequency, and summary word frequency.
6
+ *
7
+ * @module aggregation/FrequencyTracker
8
+ */
9
+ export interface FrequencyEntry {
10
+ key: string;
11
+ count: number;
12
+ firstSeen: string;
13
+ lastSeen: string;
14
+ }
15
+ export interface SerializedFrequencyState {
16
+ entries: Array<[string, {
17
+ count: number;
18
+ firstSeen: string;
19
+ lastSeen: string;
20
+ }]>;
21
+ }
22
+ export declare class FrequencyTracker {
23
+ private entries;
24
+ private readonly maxEntries;
25
+ constructor(maxEntries?: number);
26
+ /** Increment frequency for a key. */
27
+ increment(key: string, timestamp?: string): void;
28
+ /** Get the top N entries by count. */
29
+ getTopN(n: number): FrequencyEntry[];
30
+ /** Get all entries sorted by count descending. */
31
+ getAll(): FrequencyEntry[];
32
+ /** Get count for a specific key. */
33
+ getCount(key: string): number;
34
+ /** Get total number of tracked keys. */
35
+ get size(): number;
36
+ /** Reset all tracked frequencies. */
37
+ reset(): void;
38
+ /** Serialize for snapshot persistence. */
39
+ serialize(): SerializedFrequencyState;
40
+ /** Restore from serialized state. */
41
+ restore(state: SerializedFrequencyState): void;
42
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ /**
3
+ * Generic frequency counter with LRU eviction.
4
+ *
5
+ * Tracks word/key frequencies with count, firstSeen, lastSeen metadata.
6
+ * Used for tool name frequency, event type frequency, and summary word frequency.
7
+ *
8
+ * @module aggregation/FrequencyTracker
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.FrequencyTracker = void 0;
12
+ const DEFAULT_MAX_ENTRIES = 10_000;
13
+ class FrequencyTracker {
14
+ entries = new Map();
15
+ maxEntries;
16
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
17
+ this.maxEntries = maxEntries;
18
+ }
19
+ /** Increment frequency for a key. */
20
+ increment(key, timestamp) {
21
+ const ts = timestamp ?? new Date().toISOString();
22
+ const existing = this.entries.get(key);
23
+ if (existing) {
24
+ existing.count++;
25
+ existing.lastSeen = ts;
26
+ // Move to end for LRU ordering (Map preserves insertion order)
27
+ this.entries.delete(key);
28
+ this.entries.set(key, existing);
29
+ }
30
+ else {
31
+ // Evict oldest if at capacity
32
+ if (this.entries.size >= this.maxEntries) {
33
+ const oldest = this.entries.keys().next().value;
34
+ if (oldest !== undefined) {
35
+ this.entries.delete(oldest);
36
+ }
37
+ }
38
+ this.entries.set(key, { count: 1, firstSeen: ts, lastSeen: ts });
39
+ }
40
+ }
41
+ /** Get the top N entries by count. */
42
+ getTopN(n) {
43
+ const sorted = Array.from(this.entries.entries())
44
+ .map(([key, data]) => ({ key, ...data }))
45
+ .sort((a, b) => b.count - a.count);
46
+ return sorted.slice(0, n);
47
+ }
48
+ /** Get all entries sorted by count descending. */
49
+ getAll() {
50
+ return this.getTopN(this.entries.size);
51
+ }
52
+ /** Get count for a specific key. */
53
+ getCount(key) {
54
+ return this.entries.get(key)?.count ?? 0;
55
+ }
56
+ /** Get total number of tracked keys. */
57
+ get size() {
58
+ return this.entries.size;
59
+ }
60
+ /** Reset all tracked frequencies. */
61
+ reset() {
62
+ this.entries.clear();
63
+ }
64
+ /** Serialize for snapshot persistence. */
65
+ serialize() {
66
+ return { entries: Array.from(this.entries.entries()) };
67
+ }
68
+ /** Restore from serialized state. */
69
+ restore(state) {
70
+ this.entries = new Map(state.entries);
71
+ }
72
+ }
73
+ exports.FrequencyTracker = FrequencyTracker;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Rolling heatmap tracker using a circular buffer of minute-buckets.
3
+ *
4
+ * Records event timestamps into minute-resolution buckets for activity
5
+ * visualization. Older buckets age out as time advances.
6
+ *
7
+ * @module aggregation/HeatmapTracker
8
+ */
9
+ export interface HeatmapBucket {
10
+ /** ISO timestamp of the minute this bucket represents. */
11
+ timestamp: string;
12
+ /** Number of events in this minute. */
13
+ count: number;
14
+ }
15
+ export interface SerializedHeatmapState {
16
+ buckets: Array<{
17
+ minuteKey: number;
18
+ count: number;
19
+ }>;
20
+ oldestMinuteKey: number;
21
+ }
22
+ export declare class HeatmapTracker {
23
+ private buckets;
24
+ private readonly bucketCount;
25
+ constructor(bucketCount?: number);
26
+ /** Record an event at the given timestamp. */
27
+ record(timestamp: string): void;
28
+ /** Get the rolling window of buckets, filled with zeros for empty minutes. */
29
+ getBuckets(): HeatmapBucket[];
30
+ /** Get the maximum count in any bucket (for intensity scaling). */
31
+ getMaxCount(): number;
32
+ /** Reset all tracked data. */
33
+ reset(): void;
34
+ /** Serialize for snapshot persistence. */
35
+ serialize(): SerializedHeatmapState;
36
+ /** Restore from serialized state. */
37
+ restore(state: SerializedHeatmapState): void;
38
+ /** Prune buckets outside the rolling window. */
39
+ private prune;
40
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ /**
3
+ * Rolling heatmap tracker using a circular buffer of minute-buckets.
4
+ *
5
+ * Records event timestamps into minute-resolution buckets for activity
6
+ * visualization. Older buckets age out as time advances.
7
+ *
8
+ * @module aggregation/HeatmapTracker
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.HeatmapTracker = void 0;
12
+ const DEFAULT_BUCKET_COUNT = 60;
13
+ /** Truncate a Date to minute resolution and return the minute-key (epoch minutes). */
14
+ function toMinuteKey(date) {
15
+ return Math.floor(date.getTime() / 60_000);
16
+ }
17
+ class HeatmapTracker {
18
+ buckets = new Map(); // minuteKey -> count
19
+ bucketCount;
20
+ constructor(bucketCount = DEFAULT_BUCKET_COUNT) {
21
+ this.bucketCount = bucketCount;
22
+ }
23
+ /** Record an event at the given timestamp. */
24
+ record(timestamp) {
25
+ const date = new Date(timestamp);
26
+ if (isNaN(date.getTime()))
27
+ return;
28
+ const minuteKey = toMinuteKey(date);
29
+ this.buckets.set(minuteKey, (this.buckets.get(minuteKey) ?? 0) + 1);
30
+ this.prune(minuteKey);
31
+ }
32
+ /** Get the rolling window of buckets, filled with zeros for empty minutes. */
33
+ getBuckets() {
34
+ if (this.buckets.size === 0)
35
+ return [];
36
+ // Find the latest minute key
37
+ let latestKey = 0;
38
+ for (const key of this.buckets.keys()) {
39
+ if (key > latestKey)
40
+ latestKey = key;
41
+ }
42
+ const result = [];
43
+ const startKey = latestKey - this.bucketCount + 1;
44
+ for (let key = startKey; key <= latestKey; key++) {
45
+ result.push({
46
+ timestamp: new Date(key * 60_000).toISOString(),
47
+ count: this.buckets.get(key) ?? 0,
48
+ });
49
+ }
50
+ return result;
51
+ }
52
+ /** Get the maximum count in any bucket (for intensity scaling). */
53
+ getMaxCount() {
54
+ let max = 0;
55
+ for (const count of this.buckets.values()) {
56
+ if (count > max)
57
+ max = count;
58
+ }
59
+ return max;
60
+ }
61
+ /** Reset all tracked data. */
62
+ reset() {
63
+ this.buckets.clear();
64
+ }
65
+ /** Serialize for snapshot persistence. */
66
+ serialize() {
67
+ let oldest = Infinity;
68
+ const entries = [];
69
+ for (const [key, count] of this.buckets) {
70
+ entries.push({ minuteKey: key, count });
71
+ if (key < oldest)
72
+ oldest = key;
73
+ }
74
+ return { buckets: entries, oldestMinuteKey: oldest === Infinity ? 0 : oldest };
75
+ }
76
+ /** Restore from serialized state. */
77
+ restore(state) {
78
+ this.buckets.clear();
79
+ for (const { minuteKey, count } of state.buckets) {
80
+ this.buckets.set(minuteKey, count);
81
+ }
82
+ }
83
+ /** Prune buckets outside the rolling window. */
84
+ prune(latestKey) {
85
+ const cutoff = latestKey - this.bucketCount;
86
+ for (const key of this.buckets.keys()) {
87
+ if (key <= cutoff) {
88
+ this.buckets.delete(key);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ exports.HeatmapTracker = HeatmapTracker;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Simplified Drain algorithm for session event clustering.
3
+ *
4
+ * Tokenizes event summaries, groups by token count, clusters by shared prefix
5
+ * tokens, and replaces variable tokens with `<*>` to produce templates.
6
+ * Surfaces repetitive patterns like "Read src/<*>.ts" (count: 5).
7
+ *
8
+ * @module aggregation/PatternExtractor
9
+ */
10
+ export interface PatternCluster {
11
+ /** Template string with <*> wildcards for variable tokens. */
12
+ template: string;
13
+ /** Number of events matching this pattern. */
14
+ count: number;
15
+ /** Up to 3 example summaries. */
16
+ examples: string[];
17
+ }
18
+ export interface SerializedPatternState {
19
+ clusters: Array<{
20
+ tokens: string[];
21
+ mask: boolean[];
22
+ count: number;
23
+ examples: string[];
24
+ }>;
25
+ }
26
+ export declare class PatternExtractor {
27
+ private readonly maxClusters;
28
+ private readonly maxDepth;
29
+ private readonly similarityThreshold;
30
+ private groups;
31
+ constructor(options?: {
32
+ maxClusters?: number;
33
+ maxDepth?: number;
34
+ similarityThreshold?: number;
35
+ });
36
+ /** Add a summary string for clustering. */
37
+ add(summary: string): void;
38
+ /** Get all patterns sorted by frequency. */
39
+ getPatterns(): PatternCluster[];
40
+ /** Reset all tracked patterns. */
41
+ reset(): void;
42
+ /** Serialize for snapshot persistence. */
43
+ serialize(): SerializedPatternState;
44
+ /** Restore from serialized state. */
45
+ restore(state: SerializedPatternState): void;
46
+ private tokenize;
47
+ private similarity;
48
+ private buildTemplate;
49
+ private totalClusters;
50
+ private evictSmallest;
51
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ /**
3
+ * Simplified Drain algorithm for session event clustering.
4
+ *
5
+ * Tokenizes event summaries, groups by token count, clusters by shared prefix
6
+ * tokens, and replaces variable tokens with `<*>` to produce templates.
7
+ * Surfaces repetitive patterns like "Read src/<*>.ts" (count: 5).
8
+ *
9
+ * @module aggregation/PatternExtractor
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.PatternExtractor = void 0;
13
+ const DEFAULT_MAX_CLUSTERS = 100;
14
+ const DEFAULT_MAX_DEPTH = 4;
15
+ const DEFAULT_SIMILARITY_THRESHOLD = 0.5;
16
+ class PatternExtractor {
17
+ maxClusters;
18
+ maxDepth;
19
+ similarityThreshold;
20
+ // Group clusters by token count for efficient lookup
21
+ groups = new Map();
22
+ constructor(options) {
23
+ this.maxClusters = options?.maxClusters ?? DEFAULT_MAX_CLUSTERS;
24
+ this.maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
25
+ this.similarityThreshold = options?.similarityThreshold ?? DEFAULT_SIMILARITY_THRESHOLD;
26
+ }
27
+ /** Add a summary string for clustering. */
28
+ add(summary) {
29
+ const tokens = this.tokenize(summary);
30
+ if (tokens.length === 0)
31
+ return;
32
+ const key = tokens.length;
33
+ let group = this.groups.get(key);
34
+ if (!group) {
35
+ group = [];
36
+ this.groups.set(key, group);
37
+ }
38
+ // Find best matching cluster
39
+ let bestCluster = null;
40
+ let bestSim = 0;
41
+ for (const cluster of group) {
42
+ const sim = this.similarity(tokens, cluster);
43
+ if (sim > bestSim) {
44
+ bestSim = sim;
45
+ bestCluster = cluster;
46
+ }
47
+ }
48
+ if (bestCluster && bestSim >= this.similarityThreshold) {
49
+ // Merge into existing cluster
50
+ bestCluster.count++;
51
+ if (bestCluster.examples.length < 3) {
52
+ bestCluster.examples.push(summary);
53
+ }
54
+ // Update mask: mark differing positions as wildcards
55
+ for (let i = 0; i < tokens.length; i++) {
56
+ if (!bestCluster.mask[i] && tokens[i] !== bestCluster.tokens[i]) {
57
+ bestCluster.mask[i] = true;
58
+ }
59
+ }
60
+ }
61
+ else {
62
+ // Create new cluster (evict least-used if at capacity)
63
+ if (this.totalClusters() >= this.maxClusters) {
64
+ this.evictSmallest();
65
+ }
66
+ group.push({
67
+ tokens: [...tokens],
68
+ mask: new Array(tokens.length).fill(false),
69
+ count: 1,
70
+ examples: [summary],
71
+ });
72
+ }
73
+ }
74
+ /** Get all patterns sorted by frequency. */
75
+ getPatterns() {
76
+ const result = [];
77
+ for (const group of this.groups.values()) {
78
+ for (const cluster of group) {
79
+ if (cluster.count < 2)
80
+ continue; // Only show patterns with 2+ matches
81
+ result.push({
82
+ template: this.buildTemplate(cluster),
83
+ count: cluster.count,
84
+ examples: [...cluster.examples],
85
+ });
86
+ }
87
+ }
88
+ return result.sort((a, b) => b.count - a.count);
89
+ }
90
+ /** Reset all tracked patterns. */
91
+ reset() {
92
+ this.groups.clear();
93
+ }
94
+ /** Serialize for snapshot persistence. */
95
+ serialize() {
96
+ const clusters = [];
97
+ for (const group of this.groups.values()) {
98
+ for (const cluster of group) {
99
+ clusters.push({
100
+ tokens: [...cluster.tokens],
101
+ mask: [...cluster.mask],
102
+ count: cluster.count,
103
+ examples: [...cluster.examples],
104
+ });
105
+ }
106
+ }
107
+ return { clusters };
108
+ }
109
+ /** Restore from serialized state. */
110
+ restore(state) {
111
+ this.groups.clear();
112
+ for (const c of state.clusters) {
113
+ const key = c.tokens.length;
114
+ let group = this.groups.get(key);
115
+ if (!group) {
116
+ group = [];
117
+ this.groups.set(key, group);
118
+ }
119
+ group.push({
120
+ tokens: [...c.tokens],
121
+ mask: [...c.mask],
122
+ count: c.count,
123
+ examples: [...c.examples],
124
+ });
125
+ }
126
+ }
127
+ // ── Private ──
128
+ tokenize(text) {
129
+ return text.split(/\s+/).filter(t => t.length > 0).slice(0, this.maxDepth * 4);
130
+ }
131
+ similarity(tokens, cluster) {
132
+ const len = Math.min(tokens.length, this.maxDepth);
133
+ let matches = 0;
134
+ for (let i = 0; i < len; i++) {
135
+ if (cluster.mask[i] || tokens[i] === cluster.tokens[i]) {
136
+ matches++;
137
+ }
138
+ }
139
+ return matches / len;
140
+ }
141
+ buildTemplate(cluster) {
142
+ return cluster.tokens
143
+ .map((tok, i) => (cluster.mask[i] ? '<*>' : tok))
144
+ .join(' ');
145
+ }
146
+ totalClusters() {
147
+ let total = 0;
148
+ for (const group of this.groups.values()) {
149
+ total += group.length;
150
+ }
151
+ return total;
152
+ }
153
+ evictSmallest() {
154
+ let minCount = Infinity;
155
+ let minGroup = null;
156
+ let minIndex = -1;
157
+ for (const group of this.groups.values()) {
158
+ for (let i = 0; i < group.length; i++) {
159
+ if (group[i].count < minCount) {
160
+ minCount = group[i].count;
161
+ minGroup = group;
162
+ minIndex = i;
163
+ }
164
+ }
165
+ }
166
+ if (minGroup && minIndex >= 0) {
167
+ minGroup.splice(minIndex, 1);
168
+ }
169
+ }
170
+ }
171
+ exports.PatternExtractor = PatternExtractor;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Session snapshot sidecar — persists aggregator state + reader position
3
+ * so that re-attaching to a session skips full replay.
4
+ *
5
+ * Snapshots are stored in ~/.config/sidekick/snapshots/{sessionId}.json.
6
+ * They are invalidated when the source file changes beyond the snapshot position.
7
+ *
8
+ * @module aggregation/snapshot
9
+ */
10
+ import type { SerializedAggregatorState } from './EventAggregator';
11
+ /** Snapshot sidecar file content. */
12
+ export interface SessionSnapshot {
13
+ /** Schema version for forward-compatibility checks. */
14
+ version: number;
15
+ /** Session identifier (from provider.getSessionId). */
16
+ sessionId: string;
17
+ /** Provider identifier (claude-code, opencode, codex). */
18
+ providerId: string;
19
+ /** Reader position at snapshot time (byte offset or timestamp). */
20
+ readerPosition: number;
21
+ /** Source file size at snapshot time (for staleness check; 0 for DB-backed). */
22
+ sourceSize: number;
23
+ /** ISO timestamp when snapshot was written. */
24
+ createdAt: string;
25
+ /** Serialized EventAggregator state. */
26
+ aggregator: SerializedAggregatorState;
27
+ /** Serialized consumer-specific state (SessionMonitor fields, DashboardState fields, etc.). */
28
+ consumer: Record<string, unknown>;
29
+ }
30
+ /** Returns the path to a snapshot file for a given session. */
31
+ export declare function getSnapshotPath(sessionId: string): string;
32
+ /**
33
+ * Saves a session snapshot to disk.
34
+ *
35
+ * Creates the snapshots directory if it doesn't exist.
36
+ * Writes atomically via rename to avoid partial reads.
37
+ */
38
+ export declare function saveSnapshot(snapshot: SessionSnapshot): void;
39
+ /**
40
+ * Loads a session snapshot from disk if it exists and is valid.
41
+ *
42
+ * Returns null if:
43
+ * - No snapshot file exists
44
+ * - Schema version doesn't match
45
+ * - JSON parse fails
46
+ *
47
+ * The caller is responsible for staleness checks (comparing sourceSize
48
+ * and readerPosition against the actual file).
49
+ */
50
+ export declare function loadSnapshot(sessionId: string): SessionSnapshot | null;
51
+ /**
52
+ * Deletes a session snapshot file.
53
+ */
54
+ export declare function deleteSnapshot(sessionId: string): void;
55
+ /**
56
+ * Validates whether a snapshot is still usable for a given source file.
57
+ *
58
+ * For JSONL providers: checks that the file hasn't been truncated
59
+ * (current size >= snapshot position) and that the size matches or has grown.
60
+ *
61
+ * For DB-backed providers: sourceSize is 0, so we always consider valid
62
+ * (the DB reader's timestamp-based cursor handles staleness naturally).
63
+ */
64
+ export declare function isSnapshotValid(snapshot: SessionSnapshot, currentSourceSize: number): boolean;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ /**
3
+ * Session snapshot sidecar — persists aggregator state + reader position
4
+ * so that re-attaching to a session skips full replay.
5
+ *
6
+ * Snapshots are stored in ~/.config/sidekick/snapshots/{sessionId}.json.
7
+ * They are invalidated when the source file changes beyond the snapshot position.
8
+ *
9
+ * @module aggregation/snapshot
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.getSnapshotPath = getSnapshotPath;
46
+ exports.saveSnapshot = saveSnapshot;
47
+ exports.loadSnapshot = loadSnapshot;
48
+ exports.deleteSnapshot = deleteSnapshot;
49
+ exports.isSnapshotValid = isSnapshotValid;
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ const paths_1 = require("../paths");
53
+ /** Current schema version — bump when format changes. */
54
+ const SNAPSHOT_VERSION = 1;
55
+ /** Returns the directory for snapshot files. */
56
+ function getSnapshotsDir() {
57
+ return path.join((0, paths_1.getConfigDir)(), 'snapshots');
58
+ }
59
+ /** Returns the path to a snapshot file for a given session. */
60
+ function getSnapshotPath(sessionId) {
61
+ // Sanitize sessionId for filesystem safety (remove path separators)
62
+ const safe = sessionId.replace(/[/\\:]/g, '_');
63
+ return path.join(getSnapshotsDir(), `${safe}.json`);
64
+ }
65
+ /**
66
+ * Saves a session snapshot to disk.
67
+ *
68
+ * Creates the snapshots directory if it doesn't exist.
69
+ * Writes atomically via rename to avoid partial reads.
70
+ */
71
+ function saveSnapshot(snapshot) {
72
+ try {
73
+ const dir = getSnapshotsDir();
74
+ if (!fs.existsSync(dir)) {
75
+ fs.mkdirSync(dir, { recursive: true });
76
+ }
77
+ const filePath = getSnapshotPath(snapshot.sessionId);
78
+ const tmpPath = filePath + '.tmp';
79
+ fs.writeFileSync(tmpPath, JSON.stringify(snapshot), 'utf-8');
80
+ fs.renameSync(tmpPath, filePath);
81
+ }
82
+ catch {
83
+ // Non-critical — snapshot failure should never break session monitoring
84
+ }
85
+ }
86
+ /**
87
+ * Loads a session snapshot from disk if it exists and is valid.
88
+ *
89
+ * Returns null if:
90
+ * - No snapshot file exists
91
+ * - Schema version doesn't match
92
+ * - JSON parse fails
93
+ *
94
+ * The caller is responsible for staleness checks (comparing sourceSize
95
+ * and readerPosition against the actual file).
96
+ */
97
+ function loadSnapshot(sessionId) {
98
+ try {
99
+ const filePath = getSnapshotPath(sessionId);
100
+ if (!fs.existsSync(filePath)) {
101
+ return null;
102
+ }
103
+ const raw = fs.readFileSync(filePath, 'utf-8');
104
+ const snapshot = JSON.parse(raw);
105
+ // Version check
106
+ if (snapshot.version !== SNAPSHOT_VERSION) {
107
+ deleteSnapshot(sessionId);
108
+ return null;
109
+ }
110
+ return snapshot;
111
+ }
112
+ catch {
113
+ // Corrupt or unreadable — delete and move on
114
+ deleteSnapshot(sessionId);
115
+ return null;
116
+ }
117
+ }
118
+ /**
119
+ * Deletes a session snapshot file.
120
+ */
121
+ function deleteSnapshot(sessionId) {
122
+ try {
123
+ const filePath = getSnapshotPath(sessionId);
124
+ if (fs.existsSync(filePath)) {
125
+ fs.unlinkSync(filePath);
126
+ }
127
+ }
128
+ catch {
129
+ // Best-effort cleanup
130
+ }
131
+ }
132
+ /**
133
+ * Validates whether a snapshot is still usable for a given source file.
134
+ *
135
+ * For JSONL providers: checks that the file hasn't been truncated
136
+ * (current size >= snapshot position) and that the size matches or has grown.
137
+ *
138
+ * For DB-backed providers: sourceSize is 0, so we always consider valid
139
+ * (the DB reader's timestamp-based cursor handles staleness naturally).
140
+ */
141
+ function isSnapshotValid(snapshot, currentSourceSize) {
142
+ // DB-backed providers store sourceSize=0 — always valid (cursor is a timestamp)
143
+ if (snapshot.sourceSize === 0) {
144
+ return true;
145
+ }
146
+ // File was truncated or rewritten (smaller than when we snapshotted)
147
+ if (currentSourceSize < snapshot.readerPosition) {
148
+ return false;
149
+ }
150
+ return true;
151
+ }