smart-context-mcp 1.18.0 → 1.18.1

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.18.0 (or later)
59
+ # Should show: smart-context-mcp@1.18.1 (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.18.0",
4
+ "version": "1.18.1",
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",
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.18.0",
9
+ "version": "1.18.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "smart-context-mcp",
14
- "version": "1.18.0",
14
+ "version": "1.18.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -21,6 +21,12 @@ import {
21
21
  normalizeWhitespace,
22
22
  truncate,
23
23
  } from '../policy/event-policy.js';
24
+ import {
25
+ evaluateSoftPrompt,
26
+ isSoftPromptsEnabled,
27
+ markSoftPromptEmitted,
28
+ shouldEmitSoftPrompt,
29
+ } from '../policy/soft-prompts.js';
24
30
 
25
31
  export const HOOK_CLIENT = 'claude';
26
32
  export const STOP_MAX_TOKENS = 300;
@@ -501,6 +507,26 @@ export const createClaudeAdapter = ({
501
507
  continuityState: existing.continuityState,
502
508
  });
503
509
  }
510
+
511
+ if (isSoftPromptsEnabled() && shouldEmitSoftPrompt(hookKey)) {
512
+ const softPrompt = evaluateSoftPrompt({
513
+ toolName: input.tool_name,
514
+ toolInput: input.tool_input,
515
+ toolResponse: input.tool_response,
516
+ state: nextState,
517
+ });
518
+ if (softPrompt) {
519
+ markSoftPromptEmitted(hookKey);
520
+ await recordHookMetrics({
521
+ action: 'PostToolUse',
522
+ sessionId: existing.projectSessionId,
523
+ additionalContext: softPrompt.message,
524
+ continuityState: existing.continuityState,
525
+ });
526
+ return buildClaudeHookContextResponse('PostToolUse', softPrompt.message);
527
+ }
528
+ }
529
+
504
530
  return null;
505
531
  };
506
532
 
@@ -21,6 +21,12 @@ import {
21
21
  normalizeWhitespace,
22
22
  truncate,
23
23
  } from '../policy/event-policy.js';
24
+ import {
25
+ evaluateSoftPrompt,
26
+ isSoftPromptsEnabled,
27
+ markSoftPromptEmitted,
28
+ shouldEmitSoftPrompt,
29
+ } from '../policy/soft-prompts.js';
24
30
 
25
31
  export const HOOK_CLIENT = 'cursor';
26
32
  export const STOP_MAX_TOKENS = 300;
@@ -504,6 +510,26 @@ export const createCursorAdapter = ({
504
510
  continuityState: existing.continuityState,
505
511
  });
506
512
  }
513
+
514
+ if (isSoftPromptsEnabled() && shouldEmitSoftPrompt(hookKey)) {
515
+ const softPrompt = evaluateSoftPrompt({
516
+ toolName: input.tool_name,
517
+ toolInput: input.tool_input,
518
+ toolResponse: input.tool_response,
519
+ state: nextState,
520
+ });
521
+ if (softPrompt) {
522
+ markSoftPromptEmitted(hookKey);
523
+ await recordHookMetrics({
524
+ action: 'PostToolUse',
525
+ sessionId: existing.projectSessionId,
526
+ additionalContext: softPrompt.message,
527
+ continuityState: existing.continuityState,
528
+ });
529
+ return buildCursorHookContextResponse('PostToolUse', softPrompt.message);
530
+ }
531
+ }
532
+
507
533
  return null;
508
534
  };
509
535
 
@@ -0,0 +1,97 @@
1
+ const DEFAULT_THROTTLE_MS = 2 * 60 * 1000;
2
+ const LARGE_RESPONSE_CHARS = 12000;
3
+ const REPEATED_READ_THRESHOLD = 5;
4
+ const REPEATED_GREP_THRESHOLD = 3;
5
+
6
+ const lastIssuedAt = new Map();
7
+
8
+ const env = (key) => (typeof process !== 'undefined' ? process.env?.[key] : undefined);
9
+
10
+ export const isSoftPromptsEnabled = () => {
11
+ const value = env('DEVCTX_DISABLE_SOFT_PROMPTS');
12
+ if (value && /^(1|true|yes|on)$/i.test(value)) return false;
13
+ return true;
14
+ };
15
+
16
+ const measureResponseSize = (toolResponse) => {
17
+ if (!toolResponse) return 0;
18
+ if (typeof toolResponse === 'string') return toolResponse.length;
19
+ if (typeof toolResponse === 'object') {
20
+ const content = toolResponse.content ?? toolResponse.output ?? toolResponse.text ?? '';
21
+ if (typeof content === 'string') return content.length;
22
+ try { return JSON.stringify(toolResponse).length; } catch { return 0; }
23
+ }
24
+ return 0;
25
+ };
26
+
27
+ const countMatches = (state, field) => {
28
+ const value = state?.[field];
29
+ if (Array.isArray(value)) return value.length;
30
+ if (Number.isFinite(value)) return Number(value);
31
+ return 0;
32
+ };
33
+
34
+ const buildPrompt = (kind, severity, message) => ({ kind, severity, message });
35
+
36
+ export const evaluateSoftPrompt = ({ toolName, toolInput, toolResponse, state } = {}) => {
37
+ if (!toolName) return null;
38
+
39
+ const meaningfulReadCount = Number.isFinite(state?.meaningfulReadCount) ? state.meaningfulReadCount : 0;
40
+ const readFiles = Array.isArray(state?.readFiles) ? state.readFiles : [];
41
+ const touchedFiles = countMatches(state, 'touchedFiles');
42
+
43
+ if (toolName === 'Read') {
44
+ const size = measureResponseSize(toolResponse);
45
+ if (size > LARGE_RESPONSE_CHARS) {
46
+ return buildPrompt(
47
+ 'large_read',
48
+ 'med',
49
+ `devctx hint: that Read returned ~${Math.round(size / 1000)}KB. Consider smart_read({ mode: 'outline', paths: ['${toolInput?.path ?? toolInput?.file_path ?? '<path>'}'] }) for cheaper exploration, then smart_read({ mode: 'symbol' }) when you know the target.`,
50
+ );
51
+ }
52
+
53
+ if (meaningfulReadCount + 1 >= REPEATED_READ_THRESHOLD && touchedFiles === 0) {
54
+ return buildPrompt(
55
+ 'repeated_reads',
56
+ 'med',
57
+ `devctx hint: ${meaningfulReadCount + 1} sequential reads without writes. smart_context({ task: '<your task>' }) fetches curated multi-file context in one call (or smart_context({ paths: { from, to } }) to trace the import graph).`,
58
+ );
59
+ }
60
+ }
61
+
62
+ if (toolName === 'Grep' || toolName === 'SemanticSearch') {
63
+ const grepHits = readFiles.filter((p) => typeof p === 'string').length;
64
+ if (grepHits + 1 >= REPEATED_GREP_THRESHOLD) {
65
+ return buildPrompt(
66
+ 'repeated_search',
67
+ 'med',
68
+ `devctx hint: ${grepHits + 1} search calls already in this turn. smart_search({ query, intent: 'debug'|'implementation'|'explore', kinds: [...] }) ranks results by relevance and supports ADR/class/function filters.`,
69
+ );
70
+ }
71
+ }
72
+
73
+ return null;
74
+ };
75
+
76
+ export const shouldEmitSoftPrompt = (hookKey, now = Date.now(), throttleMs = DEFAULT_THROTTLE_MS) => {
77
+ if (!hookKey) return false;
78
+ const last = lastIssuedAt.get(hookKey);
79
+ if (last && now - last < throttleMs) return false;
80
+ return true;
81
+ };
82
+
83
+ export const markSoftPromptEmitted = (hookKey, now = Date.now()) => {
84
+ if (hookKey) lastIssuedAt.set(hookKey, now);
85
+ };
86
+
87
+ export const _resetSoftPromptThrottle = (hookKey) => {
88
+ if (hookKey) lastIssuedAt.delete(hookKey);
89
+ else lastIssuedAt.clear();
90
+ };
91
+
92
+ export const _internal = {
93
+ DEFAULT_THROTTLE_MS,
94
+ LARGE_RESPONSE_CHARS,
95
+ REPEATED_READ_THRESHOLD,
96
+ REPEATED_GREP_THRESHOLD,
97
+ };