smart-context-mcp 1.9.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.9.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.9.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.9.0",
9
+ "version": "1.11.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "smart-context-mcp",
14
- "version": "1.9.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);
@@ -99,39 +99,35 @@ const parseRgLine = (line, root) => {
99
99
 
100
100
  const MAX_FILE_SIZE = '1M';
101
101
 
102
- const searchWithRipgrep = async (root, query) => {
102
+ const buildRgBaseArgs = () => {
103
103
  const args = [
104
104
  '--line-number',
105
105
  '--no-heading',
106
- '--color',
107
- 'never',
106
+ '--color', 'never',
108
107
  '--smart-case',
109
- '--fixed-strings',
110
108
  '--max-filesize', MAX_FILE_SIZE,
111
109
  ];
112
-
113
110
  for (const dir of ignoredDirs) {
114
111
  args.push('--glob', `!${dir}/**`);
115
112
  args.push('--glob', `!**/${dir}/**`);
116
113
  }
117
-
118
114
  for (const fileName of ignoredFileNames) {
119
115
  args.push('--glob', `!${fileName}`);
120
116
  }
121
-
122
117
  for (const extension of supportedGlobs) {
123
118
  args.push('--glob', extension);
124
119
  }
120
+ return args;
121
+ };
125
122
 
126
- args.push(query, '.');
127
-
123
+ const runRg = async (root, pattern, extraArgs = []) => {
124
+ const args = [...buildRgBaseArgs(), ...extraArgs, pattern, '.'];
128
125
  try {
129
126
  const { stdout } = await execFile(rgPath, args, {
130
127
  cwd: root,
131
128
  maxBuffer: 1024 * 1024 * 10,
132
129
  timeout: 15000,
133
130
  });
134
-
135
131
  return stdout
136
132
  .split('\n')
137
133
  .filter(Boolean)
@@ -140,11 +136,47 @@ const searchWithRipgrep = async (root, query) => {
140
136
  .filter((match) => !shouldIgnoreFile(match.file));
141
137
  } catch (error) {
142
138
  if (error.code === 1) return [];
143
- console.error('[smart-search] ripgrep failed:', error.message, { code: error.code, signal: error.signal });
139
+ process.stderr.write(`[smart-search] ripgrep failed: ${error.message}\n`);
144
140
  return null;
145
141
  }
146
142
  };
147
143
 
144
+ const extractTerms = (query) =>
145
+ query
146
+ .split(/[\s,;|/\\]+/)
147
+ .map((t) => t.trim())
148
+ .filter((t) => t.length >= 3);
149
+
150
+ const searchWithRipgrep = async (root, query) => {
151
+ // Pass 1: exact literal match
152
+ const exact = await runRg(root, query, ['--fixed-strings']);
153
+ if (exact === null) return null;
154
+ if (exact.length > 0) return { matches: exact, searchMode: 'exact' };
155
+
156
+ // Pass 2: regex (handles partial words, snake_case, camelCase fragments)
157
+ const escaped = query.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&');
158
+ const regex = await runRg(root, escaped);
159
+ if (regex === null) return null;
160
+ if (regex.length > 0) return { matches: regex, searchMode: 'regex' };
161
+
162
+ // Pass 3: term expansion — search each significant word independently and merge
163
+ const terms = extractTerms(query);
164
+ if (terms.length < 2) return { matches: [], searchMode: 'exact', zeroReason: 'no_matches' };
165
+
166
+ const seen = new Set();
167
+ const merged = [];
168
+ for (const term of terms) {
169
+ const hits = await runRg(root, term, ['--fixed-strings']);
170
+ if (!hits) continue;
171
+ for (const hit of hits) {
172
+ const key = `${hit.file}:${hit.lineNumber}`;
173
+ if (!seen.has(key)) { seen.add(key); merged.push({ ...hit, matchedTerm: term }); }
174
+ }
175
+ }
176
+
177
+ return { matches: merged, searchMode: 'terms', terms };
178
+ };
179
+
148
180
  const MAX_FALLBACK_FILE_BYTES = 1024 * 1024;
149
181
 
150
182
  export const isSmartCaseSensitive = (query) => query !== query.toLowerCase();
@@ -273,16 +305,43 @@ const groupMatches = (matches, query, intent, indexHits, graphHits) => {
273
305
  return { groups: sorted, breakdown };
274
306
  };
275
307
 
276
- const buildCompactResult = (groups, totalMatches, query, root) => {
308
+ const buildZeroResultsMessage = (query, searchMode, provenance) => {
309
+ const lines = [`No matches found for: "${query}"`];
310
+
311
+ if (searchMode === 'exact') {
312
+ lines.push('• Tried: exact literal match (--fixed-strings)');
313
+ lines.push('• Tried: regex match');
314
+ } else if (searchMode === 'terms') {
315
+ const terms = provenance?.expandedTerms ?? [];
316
+ lines.push(`• Tried: exact, regex, and term expansion (${terms.join(', ')})`);
317
+ }
318
+
319
+ lines.push('');
320
+ lines.push('Suggestions:');
321
+ lines.push(' – Use a shorter, more specific term (e.g. a function name, not a phrase)');
322
+ lines.push(' – Try Grep for raw text: the query may be in a file type not indexed by smart_search');
323
+ lines.push(' – Run build_index to enable symbol-level search if the codebase is new');
324
+
325
+ return lines.join('\n');
326
+ };
327
+
328
+ const buildCompactResult = (groups, totalMatches, query, root, searchMode, provenance) => {
329
+ if (totalMatches === 0) {
330
+ return buildZeroResultsMessage(query, searchMode, provenance);
331
+ }
332
+
333
+ const modeLabel = searchMode === 'exact' ? '' : searchMode === 'regex' ? ' [regex fallback]' : ` [term expansion: ${(provenance?.expandedTerms ?? []).join(', ')}]`;
334
+
277
335
  if (totalMatches <= 20) {
278
- return groups
336
+ const header = modeLabel ? `# Search mode:${modeLabel}\n\n` : '';
337
+ return header + groups
279
338
  .flatMap((group) => group.matches)
280
339
  .map(formatMatch)
281
340
  .join('\n');
282
341
  }
283
342
 
284
343
  const lines = [
285
- `query: ${query}`,
344
+ `query: ${query}${modeLabel}`,
286
345
  `root: ${root}`,
287
346
  `total matches: ${totalMatches}`,
288
347
  `matched files: ${groups.length}`,
@@ -314,12 +373,13 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
314
373
  }
315
374
 
316
375
  const root = resolveSafePath(cwd);
317
- const rgMatches = _testForceWalk ? null : await searchWithRipgrep(root, query);
318
- const usedFallback = rgMatches === null;
376
+ const rgResult = _testForceWalk ? null : await searchWithRipgrep(root, query);
377
+ const usedFallback = rgResult === null;
319
378
  const engine = usedFallback ? 'walk' : 'rg';
320
379
 
321
380
  let rawMatches;
322
381
  let provenance;
382
+ let searchMode = 'exact';
323
383
 
324
384
  if (usedFallback) {
325
385
  const fallback = searchWithFallback(root, query);
@@ -340,7 +400,9 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
340
400
  warnings,
341
401
  };
342
402
  } else {
343
- rawMatches = rgMatches;
403
+ rawMatches = rgResult.matches;
404
+ searchMode = rgResult.searchMode;
405
+ if (rgResult.terms) provenance = { expandedTerms: rgResult.terms };
344
406
  }
345
407
 
346
408
  rawMatches = rawMatches.filter((match) => !shouldIgnoreFile(match.file));
@@ -403,7 +465,7 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
403
465
  }
404
466
 
405
467
  const rawText = dedupedMatches.map(formatMatch).join('\n');
406
- const compressedText = truncate(buildCompactResult(groups, dedupedMatches.length, query, root), 5000);
468
+ const compressedText = truncate(buildCompactResult(groups, dedupedMatches.length, query, root, searchMode, provenance), 5000);
407
469
  const metrics = buildMetrics({
408
470
  tool: 'smart_search',
409
471
  target: `${root} :: ${query}`,
@@ -413,17 +475,13 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
413
475
 
414
476
  await persistMetrics(metrics);
415
477
 
416
- // Record usage for feedback
417
478
  recordToolUsage({
418
479
  tool: 'smart_search',
419
480
  savedTokens: metrics.savedTokens,
420
481
  target: query,
421
482
  });
422
-
423
- // Record devctx operation for missed opportunity detection
424
483
  recordDevctxOperation();
425
-
426
- // Record decision explanation
484
+
427
485
  let reason = DECISION_REASONS.MULTIPLE_FILES;
428
486
  if (validIntent) {
429
487
  reason = DECISION_REASONS.INTENT_AWARE;
@@ -442,9 +500,11 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
442
500
  });
443
501
 
444
502
  let retrievalConfidence = 'high';
445
- if (provenance) {
446
- retrievalConfidence = provenance.skippedItemsTotal > 0 ? 'low' : 'medium';
447
- }
503
+ if (dedupedMatches.length === 0) retrievalConfidence = 'none';
504
+ else if (searchMode === 'terms') retrievalConfidence = 'low';
505
+ else if (searchMode === 'regex') retrievalConfidence = 'medium';
506
+ else if (usedFallback) retrievalConfidence = provenance?.skippedItemsTotal > 0 ? 'low' : 'medium';
507
+ else if (provenance?.skippedItemsTotal > 0) retrievalConfidence = 'low';
448
508
 
449
509
  const confidence = { level: retrievalConfidence, indexFreshness };
450
510
 
@@ -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 {