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,307 @@
1
+ import { smartMetrics } from './smart-metrics.js';
2
+ import { smartSummary } from './smart-summary.js';
3
+
4
+ const DEFAULT_START_MAX_TOKENS = 400;
5
+ const DEFAULT_END_MAX_TOKENS = 500;
6
+ const DEFAULT_END_EVENT = 'milestone';
7
+ const MAX_PROMPT_PREVIEW = 160;
8
+ const STOP_WORDS = new Set([
9
+ 'the', 'and', 'for', 'with', 'that', 'this', 'from', 'into', 'when', 'where',
10
+ 'what', 'have', 'will', 'your', 'about', 'there', 'their', 'then', 'than',
11
+ 'want', 'need', 'make', 'does', 'just', 'each', 'also', 'todo', 'done',
12
+ 'pero', 'para', 'como', 'esta', 'esto', 'cuando', 'donde', 'quiero', 'hacer',
13
+ 'sobre', 'porque', 'tengo', 'vamos', 'luego', 'ahora', 'puede', 'puedo',
14
+ ]);
15
+
16
+ const normalizeWhitespace = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
17
+
18
+ const truncate = (value, maxLength = MAX_PROMPT_PREVIEW) => {
19
+ const normalized = normalizeWhitespace(value);
20
+ if (normalized.length <= maxLength) {
21
+ return normalized;
22
+ }
23
+
24
+ if (maxLength <= 3) {
25
+ return '';
26
+ }
27
+
28
+ return `${normalized.slice(0, maxLength - 3)}...`;
29
+ };
30
+
31
+ const extractTerms = (value) => {
32
+ const normalized = normalizeWhitespace(value).toLowerCase();
33
+ if (!normalized) {
34
+ return [];
35
+ }
36
+
37
+ return [...new Set(
38
+ normalized
39
+ .split(/[^a-z0-9_.-]+/i)
40
+ .map((term) => term.trim())
41
+ .filter((term) => term.length >= 3 && !STOP_WORDS.has(term))
42
+ )];
43
+ };
44
+
45
+ const collectSummaryStrings = (value, sink = []) => {
46
+ if (typeof value === 'string') {
47
+ if (value.trim()) {
48
+ sink.push(value);
49
+ }
50
+ return sink;
51
+ }
52
+
53
+ if (Array.isArray(value)) {
54
+ value.forEach((item) => collectSummaryStrings(item, sink));
55
+ return sink;
56
+ }
57
+
58
+ if (value && typeof value === 'object') {
59
+ Object.values(value).forEach((item) => collectSummaryStrings(item, sink));
60
+ }
61
+
62
+ return sink;
63
+ };
64
+
65
+ const summarizeMetrics = (metrics) => {
66
+ if (!metrics?.summary) {
67
+ return null;
68
+ }
69
+
70
+ return {
71
+ count: metrics.summary.count,
72
+ savedTokens: metrics.summary.savedTokens,
73
+ savingsPct: metrics.summary.savingsPct,
74
+ topTools: metrics.summary.tools.slice(0, 3).map((tool) => ({
75
+ tool: tool.tool,
76
+ savedTokens: tool.savedTokens,
77
+ count: tool.count,
78
+ })),
79
+ };
80
+ };
81
+
82
+ const classifyContinuity = ({ prompt, summaryResult }) => {
83
+ if (!summaryResult?.found) {
84
+ if (summaryResult?.ambiguous) {
85
+ return {
86
+ state: 'ambiguous_resume',
87
+ shouldReuseContext: false,
88
+ reason: 'Multiple recent sessions matched and need an explicit choice.',
89
+ };
90
+ }
91
+
92
+ return {
93
+ state: 'cold_start',
94
+ shouldReuseContext: false,
95
+ reason: 'No persisted session was available for reuse.',
96
+ };
97
+ }
98
+
99
+ const promptTerms = extractTerms(prompt);
100
+ if (promptTerms.length === 0) {
101
+ return {
102
+ state: 'resume',
103
+ shouldReuseContext: true,
104
+ reason: 'A persisted session was found and no prompt terms were available for comparison.',
105
+ sharedTerms: [],
106
+ promptTermCount: 0,
107
+ summaryTermCount: 0,
108
+ matchScore: 1,
109
+ };
110
+ }
111
+
112
+ const summaryTerms = extractTerms(collectSummaryStrings(summaryResult.summary).join(' '));
113
+ const sharedTerms = promptTerms.filter((term) => summaryTerms.includes(term));
114
+ const matchScore = promptTerms.length === 0
115
+ ? 0
116
+ : Number((sharedTerms.length / promptTerms.length).toFixed(2));
117
+
118
+ if (sharedTerms.length >= 3 || matchScore >= 0.35) {
119
+ return {
120
+ state: 'aligned',
121
+ shouldReuseContext: true,
122
+ reason: 'Prompt terms align with persisted task context.',
123
+ sharedTerms: sharedTerms.slice(0, 8),
124
+ promptTermCount: promptTerms.length,
125
+ summaryTermCount: summaryTerms.length,
126
+ matchScore,
127
+ };
128
+ }
129
+
130
+ if (sharedTerms.length >= 1 || matchScore >= 0.15) {
131
+ return {
132
+ state: 'possible_shift',
133
+ shouldReuseContext: true,
134
+ reason: 'Prompt partially overlaps the persisted context; review before continuing.',
135
+ sharedTerms: sharedTerms.slice(0, 8),
136
+ promptTermCount: promptTerms.length,
137
+ summaryTermCount: summaryTerms.length,
138
+ matchScore,
139
+ };
140
+ }
141
+
142
+ return {
143
+ state: 'context_mismatch',
144
+ shouldReuseContext: false,
145
+ reason: 'Prompt terms do not align with the persisted session summary.',
146
+ sharedTerms: [],
147
+ promptTermCount: promptTerms.length,
148
+ summaryTermCount: summaryTerms.length,
149
+ matchScore,
150
+ };
151
+ };
152
+
153
+ const hasMeaningfulPrompt = (prompt) => {
154
+ const normalized = normalizeWhitespace(prompt);
155
+ return normalized.length >= 20 && extractTerms(normalized).length >= 4;
156
+ };
157
+
158
+ const buildAutoCreateUpdate = (prompt) => ({
159
+ goal: truncate(prompt, 120),
160
+ status: 'planning',
161
+ currentFocus: truncate(prompt, 160),
162
+ pinnedContext: [truncate(prompt, 160)],
163
+ nextStep: 'Inspect relevant code, confirm the task boundaries, and checkpoint the first milestone.',
164
+ });
165
+
166
+ const startTurn = async ({
167
+ sessionId,
168
+ prompt,
169
+ maxTokens = DEFAULT_START_MAX_TOKENS,
170
+ ensureSession = false,
171
+ includeMetrics = false,
172
+ metricsWindow = '7d',
173
+ latestMetrics = 5,
174
+ } = {}) => {
175
+ let summaryResult = await smartSummary({
176
+ action: 'get',
177
+ sessionId,
178
+ maxTokens,
179
+ });
180
+
181
+ let autoCreated = false;
182
+ if (!summaryResult.found && !summaryResult.ambiguous && ensureSession && hasMeaningfulPrompt(prompt)) {
183
+ const created = await smartSummary({
184
+ action: 'update',
185
+ update: buildAutoCreateUpdate(prompt),
186
+ maxTokens,
187
+ });
188
+ autoCreated = !created.blocked;
189
+ if (autoCreated) {
190
+ summaryResult = await smartSummary({
191
+ action: 'get',
192
+ sessionId: created.sessionId,
193
+ maxTokens,
194
+ });
195
+ }
196
+ }
197
+
198
+ const continuity = classifyContinuity({ prompt, summaryResult });
199
+ const effectiveSessionId = summaryResult.sessionId ?? sessionId ?? summaryResult.recommendedSessionId ?? null;
200
+ const metrics = includeMetrics
201
+ ? await smartMetrics({
202
+ window: metricsWindow,
203
+ latest: latestMetrics,
204
+ sessionId: effectiveSessionId || 'active',
205
+ })
206
+ : null;
207
+
208
+ return {
209
+ phase: 'start',
210
+ promptPreview: truncate(prompt, MAX_PROMPT_PREVIEW),
211
+ sessionId: effectiveSessionId,
212
+ found: summaryResult.found ?? false,
213
+ autoCreated,
214
+ continuity,
215
+ summary: summaryResult.summary ?? null,
216
+ repoSafety: summaryResult.repoSafety ?? metrics?.repoSafety ?? null,
217
+ sideEffectsSuppressed: Boolean(summaryResult.sideEffectsSuppressed ?? metrics?.sideEffectsSuppressed),
218
+ ...(summaryResult.candidates ? { candidates: summaryResult.candidates } : {}),
219
+ ...(summaryResult.recommendedSessionId ? { recommendedSessionId: summaryResult.recommendedSessionId } : {}),
220
+ ...(metrics ? { metrics: summarizeMetrics(metrics) } : {}),
221
+ message: summaryResult.found
222
+ ? continuity.reason
223
+ : autoCreated
224
+ ? 'Created a new persisted session for this task prompt.'
225
+ : continuity.reason,
226
+ };
227
+ };
228
+
229
+ const endTurn = async ({
230
+ sessionId,
231
+ event = DEFAULT_END_EVENT,
232
+ update = {},
233
+ force = false,
234
+ maxTokens = DEFAULT_END_MAX_TOKENS,
235
+ includeMetrics = false,
236
+ metricsWindow = '7d',
237
+ latestMetrics = 5,
238
+ } = {}) => {
239
+ const checkpoint = await smartSummary({
240
+ action: 'checkpoint',
241
+ sessionId,
242
+ event,
243
+ update,
244
+ force,
245
+ maxTokens,
246
+ });
247
+
248
+ const effectiveSessionId = checkpoint.sessionId ?? sessionId ?? 'active';
249
+ const metrics = includeMetrics
250
+ ? await smartMetrics({
251
+ window: metricsWindow,
252
+ latest: latestMetrics,
253
+ sessionId: effectiveSessionId,
254
+ })
255
+ : null;
256
+
257
+ return {
258
+ phase: 'end',
259
+ sessionId: checkpoint.sessionId ?? sessionId ?? null,
260
+ checkpoint,
261
+ repoSafety: checkpoint.repoSafety ?? metrics?.repoSafety ?? null,
262
+ sideEffectsSuppressed: Boolean(checkpoint.sideEffectsSuppressed ?? metrics?.sideEffectsSuppressed),
263
+ ...(metrics ? { metrics: summarizeMetrics(metrics) } : {}),
264
+ message: checkpoint.message,
265
+ };
266
+ };
267
+
268
+ export const smartTurn = async ({
269
+ phase,
270
+ sessionId,
271
+ prompt,
272
+ update,
273
+ event,
274
+ force,
275
+ maxTokens,
276
+ ensureSession = false,
277
+ includeMetrics = false,
278
+ metricsWindow = '7d',
279
+ latestMetrics = 5,
280
+ } = {}) => {
281
+ if (phase === 'start') {
282
+ return startTurn({
283
+ sessionId,
284
+ prompt,
285
+ maxTokens,
286
+ ensureSession,
287
+ includeMetrics,
288
+ metricsWindow,
289
+ latestMetrics,
290
+ });
291
+ }
292
+
293
+ if (phase === 'end') {
294
+ return endTurn({
295
+ sessionId,
296
+ event,
297
+ update,
298
+ force,
299
+ maxTokens,
300
+ includeMetrics,
301
+ metricsWindow,
302
+ latestMetrics,
303
+ });
304
+ }
305
+
306
+ throw new Error('Invalid phase. Valid phases: start, end');
307
+ };
@@ -14,10 +14,22 @@ const readArgValue = (name) => {
14
14
  return process.argv[index + 1] ?? null;
15
15
  };
16
16
 
17
+ const readEnvValue = (...names) => {
18
+ for (const name of names) {
19
+ const value = process.env[name]?.trim();
20
+
21
+ if (value) {
22
+ return value;
23
+ }
24
+ }
25
+
26
+ return null;
27
+ };
28
+
17
29
  const defaultDevctxRoot = path.resolve(currentDir, '..', '..');
18
30
  const defaultProjectRoot = path.resolve(defaultDevctxRoot, '..', '..');
19
31
  const projectRootArg = readArgValue('--project-root');
20
- const projectRootEnv = process.env.DEVCTX_PROJECT_ROOT ?? null;
32
+ const projectRootEnv = readEnvValue('DEVCTX_PROJECT_ROOT', 'MCP_PROJECT_ROOT');
21
33
  const rawProjectRoot = projectRootArg ?? projectRootEnv ?? defaultProjectRoot;
22
34
 
23
35
  export const devctxRoot = defaultDevctxRoot;