smart-context-mcp 1.10.0 → 1.11.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.
package/README.md CHANGED
@@ -56,7 +56,7 @@ Restart your AI client. Done.
56
56
  # Check installed version
57
57
  npm list -g smart-context-mcp
58
58
 
59
- # Should show: smart-context-mcp@1.10.0 (or later)
59
+ # Should show: smart-context-mcp@1.11.0 (or later)
60
60
 
61
61
  # Update to latest version
62
62
  npm update -g smart-context-mcp
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "smart-context-mcp",
3
3
  "mcpName": "io.github.Arrayo/smart-context-mcp",
4
- "version": "1.10.0",
4
+ "version": "1.11.0",
5
5
  "description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
6
6
  "author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
7
7
  "type": "module",
@@ -461,11 +461,9 @@ const updateCursorRule = (targetDir, dryRun) => {
461
461
  const rulesDir = path.join(targetDir, '.cursor', 'rules');
462
462
  const profilesDir = path.join(rulesDir, 'profiles-compact');
463
463
 
464
- // Write base rule (always active)
465
464
  const baseFilePath = path.join(rulesDir, 'devctx.mdc');
466
465
  writeFile(baseFilePath, cursorRuleContent, dryRun);
467
-
468
- // Write profiles README
466
+
469
467
  const profilesReadmePath = path.join(profilesDir, 'README.md');
470
468
  writeFile(profilesReadmePath, cursorProfilesNote, dryRun);
471
469
 
@@ -73,7 +73,6 @@ const calculateAdoptionMetrics = (days = 30) => {
73
73
  return withStateDb((db) => {
74
74
  const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
75
75
 
76
- // Get all sessions since cutoff
77
76
  const sessions = db
78
77
  .prepare(
79
78
  `
@@ -94,7 +93,6 @@ const calculateAdoptionMetrics = (days = 30) => {
94
93
  toolUsage: {},
95
94
  };
96
95
 
97
- // Initialize workflow stats
98
96
  Object.keys(WORKFLOW_DEFINITIONS).forEach((type) => {
99
97
  results.byWorkflow[type] = {
100
98
  total: 0,
@@ -103,12 +101,10 @@ const calculateAdoptionMetrics = (days = 30) => {
103
101
  };
104
102
  });
105
103
 
106
- // Analyze each session
107
104
  sessions.forEach((session) => {
108
105
  const snapshot = JSON.parse(session.snapshot_json || '{}');
109
106
  const sessionId = session.session_id;
110
107
 
111
- // Get events for this session
112
108
  const sessionEvents = db
113
109
  .prepare('SELECT * FROM session_events WHERE session_id = ?')
114
110
  .all(sessionId);
@@ -117,25 +113,21 @@ const calculateAdoptionMetrics = (days = 30) => {
117
113
  .prepare('SELECT * FROM metrics_events WHERE session_id = ?')
118
114
  .all(sessionId);
119
115
 
120
- // Check if non-trivial
121
116
  if (!isNonTrivialTask(sessionEvents, metricsEvents)) {
122
117
  return;
123
118
  }
124
119
 
125
120
  results.nonTrivialTasks++;
126
121
 
127
- // Check if used devctx
128
122
  const hasDevctx = usedDevctx(metricsEvents);
129
123
  if (hasDevctx) {
130
124
  results.tasksWithDevctx++;
131
125
  }
132
126
 
133
- // Track tool usage
134
127
  metricsEvents.forEach((m) => {
135
128
  results.toolUsage[m.tool] = (results.toolUsage[m.tool] || 0) + 1;
136
129
  });
137
130
 
138
- // Classify by workflow if possible
139
131
  const goal = snapshot.goal || '';
140
132
  let workflowType = null;
141
133
 
@@ -154,7 +146,6 @@ const calculateAdoptionMetrics = (days = 30) => {
154
146
  }
155
147
  });
156
148
 
157
- // Calculate rates
158
149
  if (results.nonTrivialTasks > 0) {
159
150
  results.adoptionRate = (results.tasksWithDevctx / results.nonTrivialTasks) * 100;
160
151
  }
@@ -171,7 +171,6 @@ const printSummary = (summary) => {
171
171
  console.log('─'.repeat(120));
172
172
  console.log('');
173
173
 
174
- // Show detailed breakdown for each workflow type
175
174
  console.log('Detailed Breakdown:');
176
175
  console.log('');
177
176
 
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/Arrayo/smart-context-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.10.0",
9
+ "version": "1.11.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "smart-context-mcp",
14
- "version": "1.10.0",
14
+ "version": "1.11.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
package/src/index.js CHANGED
@@ -726,9 +726,7 @@ export const buildIndex = (root, progress = null) => {
726
726
  if (sym.snippet) entry.snippet = sym.snippet;
727
727
  invertedIndex[key].push(entry);
728
728
  }
729
- } catch {
730
- // skip unreadable files
731
- }
729
+ } catch { /* unreadable */ }
732
730
 
733
731
  processed++;
734
732
 
@@ -141,7 +141,6 @@ export const formatMissedOpportunities = () => {
141
141
  lines.push('⚠️ **Missed devctx opportunities detected:**');
142
142
  lines.push('');
143
143
 
144
- // Show session stats
145
144
  lines.push(`**Session stats:**`);
146
145
  lines.push(`- Duration: ${analysis.sessionDuration}s`);
147
146
  lines.push(`- devctx operations: ${analysis.devctxOperations}`);
package/src/server.js CHANGED
@@ -57,10 +57,8 @@ export const createDevctxServer = () => {
57
57
  version,
58
58
  });
59
59
 
60
- // Enable streaming progress notifications
61
60
  setServerForStreaming(server);
62
61
 
63
- // Register prompts
64
62
  server.prompt(
65
63
  'use-devctx',
66
64
  'Force the agent to use devctx tools for the current task. Use this prompt at the start of your message to ensure devctx is used instead of native tools.',
@@ -128,7 +126,7 @@ This ensures optimal performance and context recovery.`,
128
126
 
129
127
  server.tool(
130
128
  'smart_read',
131
- 'Read a file with token-efficient modes. outline/signatures: compact structure (~90% savings). range: specific line range with line numbers. symbol: extract function/class/method by name (string or array for batch). full: file content capped at 12k chars. maxTokens: token budget auto-selects the most detailed mode that fits (full -> outline -> signatures -> truncated). context=true (symbol mode only): includes callers, tests, and referenced types from the dependency graph; returns graphCoverage (imports/tests: full|partial|none) so the agent knows how reliable the cross-file context is. Responses are cached in memory per session and invalidated by file mtime; cached=true when served from cache. Every response includes a unified confidence block: { parser, truncated, cached, graphCoverage? }. Supports JS/TS, Python, Go, Rust, Java, C#, Kotlin, PHP, Swift, shell, Terraform, Dockerfile, SQL, JSON, TOML, YAML.',
129
+ 'Read a file with token-efficient modes. PREFER outline/signatures/symbol full mode saves 0 tokens (same content as native Read, capped at 12k chars). Mode guide: outline (~90% savings): file structure, exports, top-level symbols — use for orientation, code review, deciding what to read next. signatures (~85% savings): function/method signatures only use when you need parameter names and return types without bodies. symbol: extract one or more functions/classes/methods by name (string or array for batch) use when you know exactly what to read; add context=true to include callers, tests, and referenced types from the dependency graph (returns graphCoverage: full|partial|none). range: specific line range with line numbers — use only when you need exact lines. full: raw file content, no compression, no savings — only use when the exact byte-for-byte content is required (e.g. config files, lock files). maxTokens: token budget — auto-selects the most detailed mode that fits (full → outline → signatures → truncated). Responses are cached in memory per session and invalidated by file mtime; cached=true when served from cache. Every response includes a unified confidence block: { parser, truncated, cached, graphCoverage? }. Supports JS/TS, Python, Go, Rust, Java, C#, Kotlin, PHP, Swift, shell, Terraform, Dockerfile, SQL, JSON, TOML, YAML.',
132
130
  {
133
131
  filePath: z.string(),
134
132
  mode: z.enum(['full', 'outline', 'signatures', 'range', 'symbol']).optional(),
@@ -144,7 +142,7 @@ This ensures optimal performance and context recovery.`,
144
142
 
145
143
  server.tool(
146
144
  'smart_read_batch',
147
- 'Read multiple files in one call. Each item accepts path, mode, symbol, startLine, endLine, maxTokens (per-file budget). Optional global maxTokens budget with early stop when exceeded. Max 20 files per call.',
145
+ 'Read multiple files in one call. Each item accepts path, mode (prefer outline/signatures/symbol — full saves 0 tokens), symbol, startLine, endLine, maxTokens (per-file budget). Optional global maxTokens budget with early stop when exceeded. Max 20 files per call.',
148
146
  {
149
147
  files: z.array(z.object({
150
148
  path: z.string(),
@@ -190,7 +188,7 @@ This ensures optimal performance and context recovery.`,
190
188
 
191
189
  server.tool(
192
190
  'smart_shell',
193
- 'Run a diagnostic shell command from an allowlist. Allowed: pwd, ls, find, rg, git (status/diff/show/log/branch/rev-parse), npm/pnpm/yarn/bun (test/run/lint/build/typecheck/check). Blocks shell operators, pipes, and unsafe commands. Includes a unified confidence block: { blocked, timedOut }.',
191
+ 'Run a diagnostic shell command from an allowlist. Allowed: pwd, ls, find, rg, git (status/diff/show/log/branch/rev-parse), npm/pnpm/yarn/bun (test/run/lint/build/typecheck/check). Blocks shell operators, pipes, and unsafe commands. For large diffs: output is split by file (up to 8 files, 60 lines each) with a hint to run git show -- <file> for the full body of any truncated file; prefer git diff --stat first to see which files changed, then git show -- <file> per file for targeted reading. Includes a unified confidence block: { blocked, timedOut }.',
194
192
  {
195
193
  command: z.string(),
196
194
  },
@@ -487,7 +485,7 @@ This ensures optimal performance and context recovery.`,
487
485
 
488
486
  server.tool(
489
487
  'smart_turn',
490
- 'Orchestrate start/end of a meaningful agent turn so context usage becomes almost mandatory with low token overhead. `phase: "start"` rehydrates persisted context, classifies prompt continuity against the saved session, optionally auto-creates a planning session for a new substantial task, returns `recommendedPath` guidance for the next safe devctx actions, and can include compact metrics. `phase: "end"` writes a checkpoint through smart_summary, returns follow-up `recommendedPath` guidance, and can optionally include compact metrics. Both phases expose `mutationSafety` when repo-safety blocks persisted writes and now surface `storageHealth` when SQLite state is missing, oversized, locked, or corrupted. Use this instead of manually chaining `smart_summary(get)` and `smart_summary(checkpoint)` when you want a single context-first turn workflow.',
488
+ 'Orchestrate start/end of a meaningful agent turn for multi-session tasks where context continuity matters. SKIP for single-session point-in-time tasks (reviewing a specific commit, answering a quick question, one-off lookup) — the setup overhead exceeds the benefit if the session will never be resumed. USE when: the task spans multiple chat sessions, you may return to it the next day, or the codebase context is large enough that re-reading is expensive. `phase: "start"` rehydrates persisted context, classifies prompt continuity against the saved session, optionally auto-creates a planning session for a new substantial task, returns `recommendedPath` guidance for the next safe devctx actions, and can include compact metrics. `phase: "end"` writes a checkpoint through smart_summary, returns follow-up `recommendedPath` guidance, and can optionally include compact metrics. Both phases expose `mutationSafety` when repo-safety blocks persisted writes and surface `storageHealth` when SQLite state is missing, oversized, locked, or corrupted.',
491
489
  {
492
490
  phase: z.enum(['start', 'end']),
493
491
  sessionId: z.string().optional(),
@@ -412,17 +412,11 @@ export const smartContext = async ({
412
412
 
413
413
  await ensureIndexReady({ root });
414
414
 
415
- // Get detailed diff stats
416
415
  const detailedChanges = await getDetailedDiff(changed.ref, root);
417
416
  const index = loadIndex(root);
418
-
419
- // Analyze impact and prioritize
420
417
  const prioritized = analyzeChangeImpact(detailedChanges, index);
421
-
422
- // Expand to include related files (importers, dependencies, tests)
423
418
  const expandedFiles = expandChangedContext(changed.files, index, 10);
424
-
425
- // Build primary seeds with priority and impact data
419
+
426
420
  primarySeeds = Array.from(expandedFiles).map(rel => {
427
421
  const changeInfo = prioritized.find(c => c.file === rel);
428
422
  const evidence = [{
@@ -432,7 +426,6 @@ export const smartContext = async ({
432
426
  impact: changeInfo?.impactScore || 0,
433
427
  }];
434
428
 
435
- // Mark files that were expanded (not directly changed)
436
429
  if (!changed.files.includes(rel)) {
437
430
  evidence[0].expanded = true;
438
431
  }
@@ -444,7 +437,6 @@ export const smartContext = async ({
444
437
  };
445
438
  });
446
439
 
447
- // Sort by impact (critical changes first)
448
440
  primarySeeds.sort((a, b) => {
449
441
  const impactA = a.evidence[0].impact || 0;
450
442
  const impactB = b.evidence[0].impact || 0;
@@ -771,17 +763,13 @@ export const smartContext = async ({
771
763
  timestamp: new Date().toISOString(),
772
764
  });
773
765
 
774
- // Record usage for feedback
775
766
  recordToolUsage({
776
767
  tool: 'smart_context',
777
768
  savedTokens,
778
769
  target: task,
779
770
  });
780
-
781
- // Record devctx operation for missed opportunity detection
782
771
  recordDevctxOperation();
783
-
784
- // Record decision explanation
772
+
785
773
  let reason = DECISION_REASONS.TASK_CONTEXT;
786
774
  if (diff) {
787
775
  reason = DECISION_REASONS.DIFF_ANALYSIS;
@@ -536,17 +536,12 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
536
536
 
537
537
  await persistMetrics(metrics);
538
538
 
539
- // Record usage for feedback
540
539
  recordToolUsage({
541
540
  tool: 'smart_read',
542
541
  savedTokens: metrics.savedTokens,
543
542
  target: path.relative(effectiveRoot, fullPath),
544
543
  });
545
-
546
- // Record devctx operation for missed opportunity detection
547
544
  recordDevctxOperation();
548
-
549
- // Record decision explanation
550
545
  const lineCount = content.split('\n').length;
551
546
  let reason = DECISION_REASONS.LARGE_FILE;
552
547
  let expectedBenefit = EXPECTED_BENEFITS.TOKEN_SAVINGS(metrics.savedTokens);
@@ -475,17 +475,13 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
475
475
 
476
476
  await persistMetrics(metrics);
477
477
 
478
- // Record usage for feedback
479
478
  recordToolUsage({
480
479
  tool: 'smart_search',
481
480
  savedTokens: metrics.savedTokens,
482
481
  target: query,
483
482
  });
484
-
485
- // Record devctx operation for missed opportunity detection
486
483
  recordDevctxOperation();
487
-
488
- // Record decision explanation
484
+
489
485
  let reason = DECISION_REASONS.MULTIPLE_FILES;
490
486
  if (validIntent) {
491
487
  reason = DECISION_REASONS.INTENT_AWARE;
@@ -176,6 +176,52 @@ const validateCommand = (command, tokens) => {
176
176
  return null;
177
177
  };
178
178
 
179
+ const DIFF_FILE_HEADER = /^diff --git a\/.+ b\/.+/;
180
+ const DIFF_HUNK_HEADER = /^@@ /;
181
+ const MAX_DIFF_FILES = 8;
182
+ const MAX_LINES_PER_FILE = 60;
183
+ const DIFF_TOTAL_LIMIT = 4000;
184
+
185
+ const splitDiffByFile = (text) => {
186
+ const files = [];
187
+ let current = null;
188
+
189
+ for (const line of text.split('\n')) {
190
+ if (DIFF_FILE_HEADER.test(line)) {
191
+ if (current) files.push(current);
192
+ current = { header: line, lines: [] };
193
+ } else if (current) {
194
+ current.lines.push(line);
195
+ }
196
+ }
197
+ if (current) files.push(current);
198
+ return files;
199
+ };
200
+
201
+ const compressDiff = (text) => {
202
+ if (!DIFF_FILE_HEADER.test(text)) return text;
203
+
204
+ const files = splitDiffByFile(text);
205
+ if (files.length === 0) return text;
206
+
207
+ const shown = files.slice(0, MAX_DIFF_FILES);
208
+ const skipped = files.length - shown.length;
209
+
210
+ const parts = shown.map(({ header, lines }) => {
211
+ const truncatedLines = lines.slice(0, MAX_LINES_PER_FILE);
212
+ const skippedLines = lines.length - truncatedLines.length;
213
+ const hunkCount = lines.filter((l) => DIFF_HUNK_HEADER.test(l)).length;
214
+ const suffix = skippedLines > 0 ? [`... (${skippedLines} more lines — use smart_read(symbol) for full body)`] : [];
215
+ return [header, `# ${hunkCount} hunk(s)`, ...truncatedLines, ...suffix].join('\n');
216
+ });
217
+
218
+ const footer = skipped > 0
219
+ ? `\n# ${skipped} more file(s) not shown — run git show -- <file> for each`
220
+ : '';
221
+
222
+ return truncate(parts.join('\n\n'), DIFF_TOTAL_LIMIT) + footer;
223
+ };
224
+
179
225
  const buildBlockedResult = async (command, message) => {
180
226
  const metrics = buildMetrics({
181
227
  tool: 'smart_shell',
@@ -254,7 +300,7 @@ export const smartShell = async ({ command }) => {
254
300
  ]);
255
301
  const shouldPrioritizeRelevant = execution.code !== 0 || execution.timedOut;
256
302
  const compressedSource = shouldPrioritizeRelevant && relevant ? relevant : rawText;
257
- const compressedText = truncate(uniqueLines(compressedSource), 5000);
303
+ const compressedText = truncate(compressDiff(uniqueLines(compressedSource)), 5000);
258
304
  const metrics = buildMetrics({
259
305
  tool: 'smart_shell',
260
306
  target: command,
@@ -264,17 +310,12 @@ export const smartShell = async ({ command }) => {
264
310
 
265
311
  await persistMetrics(metrics);
266
312
 
267
- // Record usage for feedback
268
313
  recordToolUsage({
269
314
  tool: 'smart_shell',
270
315
  savedTokens: metrics.savedTokens,
271
316
  target: command,
272
317
  });
273
-
274
- // Record devctx operation for missed opportunity detection
275
318
  recordDevctxOperation();
276
-
277
- // Record decision explanation
278
319
  const outputLines = rawText.split('\n').length;
279
320
  let reason = DECISION_REASONS.COMMAND_OUTPUT;
280
321
  if (shouldPrioritizeRelevant && relevant) {
@@ -60,7 +60,6 @@ export const formatUsageFeedback = () => {
60
60
  lines.push('');
61
61
  lines.push('📊 **devctx usage this session:**');
62
62
 
63
- // Sort by count descending
64
63
  const sorted = usage.tools.sort((a, b) => b.count - a.count);
65
64
 
66
65
  for (const { tool, count, savedTokens, targets } of sorted) {
@@ -183,7 +183,6 @@ export const endWorkflow = (workflowId) => {
183
183
  return null;
184
184
  }
185
185
 
186
- // Get workflow start time and session
187
186
  const workflow = db
188
187
  .prepare(
189
188
  `
@@ -203,7 +202,6 @@ export const endWorkflow = (workflowId) => {
203
202
  const endTime = new Date(now);
204
203
  const durationMs = endTime - startTime;
205
204
 
206
- // Get all metrics for this session since workflow start
207
205
  const metrics = db
208
206
  .prepare(
209
207
  `
@@ -215,7 +213,6 @@ export const endWorkflow = (workflowId) => {
215
213
  )
216
214
  .all(workflow.session_id, workflow.start_time);
217
215
 
218
- // Calculate totals
219
216
  const rawTokens = metrics.reduce((sum, m) => sum + (m.raw_tokens || 0), 0);
220
217
  const compressedTokens = metrics.reduce((sum, m) => sum + (m.compressed_tokens || 0), 0);
221
218
  const savedTokens = metrics.reduce((sum, m) => sum + (m.saved_tokens || 0), 0);
@@ -231,7 +228,6 @@ export const endWorkflow = (workflowId) => {
231
228
  const savingsPct = rawTokens > 0 ? ((savedTokens / rawTokens) * 100).toFixed(2) : 0;
232
229
  const netSavingsPct = rawTokens > 0 ? ((netSavedTokens / rawTokens) * 100).toFixed(2) : 0;
233
230
 
234
- // Calculate vs baseline
235
231
  const baselineTokens = workflow.baseline_tokens || 0;
236
232
  const vsBaselinePct = baselineTokens > 0 ? (((baselineTokens - compressedTokens) / baselineTokens) * 100).toFixed(2) : 0;
237
233
  const vsBaselineNetPct = baselineTokens > 0 ? (((baselineTokens - (compressedTokens + overheadTokens)) / baselineTokens) * 100).toFixed(2) : 0;
@@ -247,10 +243,7 @@ export const endWorkflow = (workflowId) => {
247
243
  },
248
244
  };
249
245
 
250
- // Get unique tools used
251
246
  const toolsUsed = [...new Set(metrics.map((m) => m.tool))];
252
-
253
- // Update workflow
254
247
  const stmt = db.prepare(`
255
248
  UPDATE workflow_metrics
256
249
  SET end_time = ?,
@@ -584,7 +577,6 @@ export const autoTrackWorkflow = (sessionId, sessionGoal) => {
584
577
  return null;
585
578
  }
586
579
 
587
- // Check if workflow already tracked for this session
588
580
  const existing = db
589
581
  .prepare(
590
582
  `
@@ -601,7 +593,6 @@ export const autoTrackWorkflow = (sessionId, sessionGoal) => {
601
593
  return existing.workflow_id;
602
594
  }
603
595
 
604
- // Get tools used so far in this session
605
596
  const metrics = db
606
597
  .prepare(
607
598
  `
@@ -621,7 +612,6 @@ export const autoTrackWorkflow = (sessionId, sessionGoal) => {
621
612
  return null;
622
613
  }
623
614
 
624
- // Start tracking
625
615
  return startWorkflow(workflowType, sessionId, { autoDetected: true, goal: sessionGoal });
626
616
  });
627
617
  } catch {