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.
- package/README.md +145 -30
- package/package.json +8 -3
- package/scripts/check-repo-safety.js +84 -0
- package/scripts/claude-hook.js +33 -0
- package/scripts/headless-wrapper.js +106 -0
- package/scripts/init-clients.js +138 -7
- package/scripts/report-metrics.js +24 -119
- package/src/hooks/claude-hooks.js +424 -0
- package/src/mcp-server.js +6 -3
- package/src/metrics.js +218 -8
- package/src/orchestration/headless-wrapper.js +314 -0
- package/src/repo-safety.js +166 -0
- package/src/server.js +83 -4
- package/src/storage/sqlite.js +1092 -0
- package/src/tools/smart-metrics.js +249 -0
- package/src/tools/smart-summary.js +1230 -324
- package/src/tools/smart-turn.js +307 -0
- package/src/utils/runtime-config.js +13 -1
|
@@ -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
|
+
};
|