smart-context-mcp 1.0.2 → 1.0.4

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,249 @@
1
+ import fs from 'node:fs';
2
+ import { enforceRepoSafety, getRepoSafety } from '../repo-safety.js';
3
+ import {
4
+ ACTIVE_SESSION_SCOPE,
5
+ importLegacyState,
6
+ withStateDb,
7
+ withStateDbSnapshot,
8
+ } from '../storage/sqlite.js';
9
+ import {
10
+ aggregateMetrics,
11
+ getCompressedTokens,
12
+ getEntrySavingsPct,
13
+ getSavedTokens,
14
+ readMetricsEntries,
15
+ resolveMetricsInput,
16
+ } from '../metrics.js';
17
+
18
+ const WINDOW_MS = {
19
+ '24h': 24 * 60 * 60 * 1000,
20
+ '7d': 7 * 24 * 60 * 60 * 1000,
21
+ '30d': 30 * 24 * 60 * 60 * 1000,
22
+ all: Infinity,
23
+ };
24
+
25
+ const toTimestamp = (entry) => {
26
+ const timestamp = Date.parse(entry.timestamp ?? '');
27
+ return Number.isFinite(timestamp) ? timestamp : 0;
28
+ };
29
+
30
+ const applyWindowFilter = (entries, window) => {
31
+ if (window === 'all') {
32
+ return entries;
33
+ }
34
+
35
+ const windowMs = WINDOW_MS[window] ?? WINDOW_MS['7d'];
36
+ const cutoff = Date.now() - windowMs;
37
+ return entries.filter((entry) => toTimestamp(entry) >= cutoff);
38
+ };
39
+
40
+ const buildLatestEntries = (entries, limit) =>
41
+ entries
42
+ .slice()
43
+ .sort((a, b) => toTimestamp(b) - toTimestamp(a))
44
+ .slice(0, limit)
45
+ .map((entry) => {
46
+ const compressedTokens = getCompressedTokens(entry);
47
+ const savedTokens = getSavedTokens(entry, compressedTokens);
48
+ return {
49
+ tool: entry.tool ?? 'unknown',
50
+ action: entry.action ?? null,
51
+ target: entry.target ?? null,
52
+ sessionId: entry.sessionId ?? null,
53
+ rawTokens: Number(entry.rawTokens ?? 0),
54
+ compressedTokens,
55
+ savedTokens,
56
+ savingsPct: getEntrySavingsPct(entry, compressedTokens, savedTokens),
57
+ overheadTokens: Math.max(0, Number(entry.metadata?.overheadTokens ?? 0)),
58
+ timestamp: entry.timestamp ?? null,
59
+ };
60
+ });
61
+
62
+ const getActiveSessionId = (db) =>
63
+ db.prepare('SELECT session_id FROM active_session WHERE scope = ?').get(ACTIVE_SESSION_SCOPE)?.session_id ?? null;
64
+ const HARD_BLOCK_REPO_SAFETY_REASONS = [
65
+ ['tracked', 'isTracked'],
66
+ ['staged', 'isStaged'],
67
+ ];
68
+
69
+ const getSqliteSafetyPolicy = () => {
70
+ const repoSafety = enforceRepoSafety();
71
+ const reasons = HARD_BLOCK_REPO_SAFETY_REASONS
72
+ .filter(([, field]) => repoSafety[field])
73
+ .map(([reason]) => reason);
74
+
75
+ return {
76
+ repoSafety,
77
+ shouldBlock: reasons.length > 0,
78
+ reasons,
79
+ };
80
+ };
81
+
82
+ const resolveSessionId = (sessionId, activeSessionId) => {
83
+ if (!sessionId) {
84
+ return null;
85
+ }
86
+
87
+ if (sessionId === 'active') {
88
+ return activeSessionId;
89
+ }
90
+
91
+ return sessionId;
92
+ };
93
+
94
+ const readSqliteMetricsEntries = async ({ tool, sessionId, window }) => {
95
+ const safety = getSqliteSafetyPolicy();
96
+ const resolved = resolveMetricsInput({});
97
+ const reader = safety.shouldBlock ? withStateDbSnapshot : withStateDb;
98
+
99
+ if (safety.shouldBlock && !fs.existsSync(resolved.storagePath)) {
100
+ return {
101
+ entries: [],
102
+ activeSessionId: null,
103
+ resolvedSessionId: resolveSessionId(sessionId, null),
104
+ invalidLines: [],
105
+ repoSafety: safety.repoSafety,
106
+ sideEffectsSuppressed: true,
107
+ };
108
+ }
109
+
110
+ if (!safety.shouldBlock) {
111
+ await importLegacyState();
112
+ }
113
+
114
+ return reader((db) => {
115
+ const activeSessionId = getActiveSessionId(db);
116
+ const resolvedSessionId = resolveSessionId(sessionId, activeSessionId);
117
+ const windowMs = WINDOW_MS[window] ?? WINDOW_MS['7d'];
118
+ const cutoff = window === 'all' ? null : new Date(Date.now() - windowMs).toISOString();
119
+
120
+ const clauses = [];
121
+ const values = [];
122
+
123
+ if (tool) {
124
+ clauses.push('tool = ?');
125
+ values.push(tool);
126
+ }
127
+
128
+ if (resolvedSessionId) {
129
+ clauses.push('session_id = ?');
130
+ values.push(resolvedSessionId);
131
+ }
132
+
133
+ if (cutoff) {
134
+ clauses.push('created_at >= ?');
135
+ values.push(cutoff);
136
+ }
137
+
138
+ const whereClause = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
139
+ const rows = db.prepare(`
140
+ SELECT
141
+ tool,
142
+ action,
143
+ session_id,
144
+ target,
145
+ raw_tokens,
146
+ compressed_tokens,
147
+ saved_tokens,
148
+ savings_pct,
149
+ metadata_json,
150
+ created_at
151
+ FROM metrics_events
152
+ ${whereClause}
153
+ ORDER BY datetime(created_at) DESC, metric_id DESC
154
+ `).all(...values);
155
+
156
+ return {
157
+ entries: rows.map((row) => ({
158
+ tool: row.tool,
159
+ action: row.action,
160
+ sessionId: row.session_id,
161
+ target: row.target,
162
+ rawTokens: row.raw_tokens,
163
+ compressedTokens: row.compressed_tokens,
164
+ savedTokens: row.saved_tokens,
165
+ savingsPct: row.savings_pct,
166
+ metadata: (() => {
167
+ try {
168
+ return JSON.parse(row.metadata_json ?? '{}');
169
+ } catch {
170
+ return {};
171
+ }
172
+ })(),
173
+ timestamp: row.created_at,
174
+ })),
175
+ activeSessionId,
176
+ resolvedSessionId,
177
+ invalidLines: [],
178
+ repoSafety: safety.shouldBlock ? safety.repoSafety : getRepoSafety(),
179
+ sideEffectsSuppressed: safety.shouldBlock,
180
+ };
181
+ }, safety.shouldBlock ? { filePath: resolved.storagePath } : undefined);
182
+ };
183
+
184
+ export const smartMetrics = async ({
185
+ file,
186
+ tool,
187
+ sessionId,
188
+ window = '7d',
189
+ latest = 10,
190
+ }) => {
191
+ const resolved = resolveMetricsInput({ file });
192
+
193
+ if (resolved.kind === 'file') {
194
+ const { entries, invalidLines } = readMetricsEntries(resolved.storagePath);
195
+ const resolvedSessionId = resolveSessionId(sessionId, null);
196
+ const filteredEntries = applyWindowFilter(entries, window)
197
+ .filter((entry) => (tool ? entry.tool === tool : true))
198
+ .filter((entry) => (resolvedSessionId ? entry.sessionId === resolvedSessionId : true));
199
+
200
+ return {
201
+ filePath: resolved.storagePath,
202
+ storagePath: resolved.storagePath,
203
+ source: resolved.source,
204
+ repoSafety: null,
205
+ sideEffectsSuppressed: false,
206
+ activeSessionId: null,
207
+ filters: {
208
+ tool: tool ?? null,
209
+ sessionId: resolvedSessionId,
210
+ window,
211
+ latest,
212
+ },
213
+ invalidLines,
214
+ summary: aggregateMetrics(filteredEntries),
215
+ latestEntries: buildLatestEntries(filteredEntries, latest),
216
+ };
217
+ }
218
+
219
+ const {
220
+ entries,
221
+ activeSessionId,
222
+ resolvedSessionId,
223
+ invalidLines,
224
+ repoSafety,
225
+ sideEffectsSuppressed,
226
+ } = await readSqliteMetricsEntries({
227
+ tool,
228
+ sessionId,
229
+ window,
230
+ });
231
+
232
+ return {
233
+ filePath: resolved.storagePath,
234
+ storagePath: resolved.storagePath,
235
+ source: resolved.source,
236
+ repoSafety,
237
+ sideEffectsSuppressed,
238
+ activeSessionId,
239
+ filters: {
240
+ tool: tool ?? null,
241
+ sessionId: resolvedSessionId,
242
+ window,
243
+ latest,
244
+ },
245
+ invalidLines,
246
+ summary: aggregateMetrics(entries),
247
+ latestEntries: buildLatestEntries(entries, latest),
248
+ };
249
+ };