smart-context-mcp 1.2.0 → 1.3.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 +130 -1
- package/package.json +1 -1
- package/src/decision-explainer.js +168 -0
- package/src/missed-opportunities.js +255 -0
- package/src/server.js +66 -0
- package/src/tools/smart-context.js +33 -1
- package/src/tools/smart-read.js +35 -0
- package/src/tools/smart-search.js +31 -0
- package/src/tools/smart-shell.js +29 -0
- package/src/tools/smart-summary.js +46 -2
- package/src/usage-feedback.js +179 -0
package/README.md
CHANGED
|
@@ -62,7 +62,8 @@ Restart your AI client. Done.
|
|
|
62
62
|
- ✅ Token savings: 85-90% on complex tasks
|
|
63
63
|
|
|
64
64
|
Check actual usage:
|
|
65
|
-
-
|
|
65
|
+
- **Real-time feedback** - See usage immediately (enable with `export DEVCTX_SHOW_USAGE=true`)
|
|
66
|
+
- `npm run report:metrics` - Tool-level savings + adoption analysis
|
|
66
67
|
- `npm run report:workflows` - Workflow-level savings (requires `DEVCTX_WORKFLOW_TRACKING=true`)
|
|
67
68
|
|
|
68
69
|
## What it does
|
|
@@ -102,6 +103,134 @@ Installation generates rules that teach agents optimal workflows:
|
|
|
102
103
|
|
|
103
104
|
Production usage: **14.5M tokens → 1.6M tokens** (89.87% reduction)
|
|
104
105
|
|
|
106
|
+
## Verify It's Working
|
|
107
|
+
|
|
108
|
+
### Real-Time Feedback (Auto-enabled for First 10 Calls)
|
|
109
|
+
|
|
110
|
+
Feedback is **automatically enabled** for your first 10 tool calls (onboarding mode), then auto-disables.
|
|
111
|
+
|
|
112
|
+
**To keep it enabled permanently:**
|
|
113
|
+
```bash
|
|
114
|
+
export DEVCTX_SHOW_USAGE=true
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**To disable immediately:**
|
|
118
|
+
```bash
|
|
119
|
+
export DEVCTX_SHOW_USAGE=false
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
You'll see at the end of agent responses:
|
|
123
|
+
|
|
124
|
+
```markdown
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
📊 **devctx usage this session:**
|
|
128
|
+
- **smart_read**: 3 calls | ~45.0K tokens saved (file1.js, file2.js, file3.js)
|
|
129
|
+
- **smart_search**: 1 call | ~12.0K tokens saved (query)
|
|
130
|
+
|
|
131
|
+
**Total saved:** ~57.0K tokens
|
|
132
|
+
|
|
133
|
+
*Onboarding mode: showing for 3 more tool calls. To keep: `export DEVCTX_SHOW_USAGE=true`*
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Why this is useful:**
|
|
137
|
+
- ✅ Verify agent is following rules
|
|
138
|
+
- ✅ See token savings in real-time
|
|
139
|
+
- ✅ Debug adoption issues instantly
|
|
140
|
+
- ✅ Validate forcing prompts worked
|
|
141
|
+
|
|
142
|
+
### Historical Metrics
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm run report:metrics
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Shows adoption analysis + token savings over time.
|
|
149
|
+
|
|
150
|
+
### Decision Explanations (Optional)
|
|
151
|
+
|
|
152
|
+
Understand **why** the agent chose devctx tools:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
export DEVCTX_EXPLAIN=true
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
You'll see explanations like:
|
|
159
|
+
|
|
160
|
+
```markdown
|
|
161
|
+
🤖 **Decision explanations:**
|
|
162
|
+
|
|
163
|
+
**smart_read** (read server.js (outline mode))
|
|
164
|
+
- **Why:** File is large (2500 lines), outline mode extracts structure only
|
|
165
|
+
- **Instead of:** Read (full file)
|
|
166
|
+
- **Expected benefit:** ~45.0K tokens saved
|
|
167
|
+
|
|
168
|
+
**smart_search** (search "bug" (intent: debug))
|
|
169
|
+
- **Why:** Intent-aware search prioritizes relevant results
|
|
170
|
+
- **Expected benefit:** Better result ranking
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**When to use:**
|
|
174
|
+
- Learning how devctx works
|
|
175
|
+
- Debugging tool selection
|
|
176
|
+
- Understanding best practices
|
|
177
|
+
|
|
178
|
+
### Missed Opportunities Detection (Optional)
|
|
179
|
+
|
|
180
|
+
Detect when devctx **should have been used but wasn't**:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
export DEVCTX_DETECT_MISSED=true
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
You'll see warnings like:
|
|
187
|
+
|
|
188
|
+
```markdown
|
|
189
|
+
⚠️ **Missed devctx opportunities detected:**
|
|
190
|
+
|
|
191
|
+
**Session stats:**
|
|
192
|
+
- devctx operations: 2
|
|
193
|
+
- Estimated total: 25
|
|
194
|
+
- Adoption: 8%
|
|
195
|
+
|
|
196
|
+
🟡 **low devctx adoption**
|
|
197
|
+
- **Issue:** Low adoption (8%). Target: >50%
|
|
198
|
+
- **Potential savings:** ~184.0K tokens
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Detects:**
|
|
202
|
+
- No devctx usage in long sessions
|
|
203
|
+
- Low adoption (<30%)
|
|
204
|
+
- Usage dropped mid-session
|
|
205
|
+
|
|
206
|
+
**Combine all features:**
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
export DEVCTX_SHOW_USAGE=true # See what's used
|
|
210
|
+
export DEVCTX_EXPLAIN=true # Understand why
|
|
211
|
+
export DEVCTX_DETECT_MISSED=true # Detect gaps
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## MCP Prompts
|
|
215
|
+
|
|
216
|
+
The MCP server provides **prompts** for automatic forcing:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
/prompt use-devctx
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Available prompts:**
|
|
223
|
+
- `use-devctx` - Ultra-short forcing prompt
|
|
224
|
+
- `devctx-workflow` - Complete workflow template
|
|
225
|
+
- `devctx-preflight` - Preflight checklist
|
|
226
|
+
|
|
227
|
+
**Benefits:**
|
|
228
|
+
- No manual typing
|
|
229
|
+
- Centrally managed
|
|
230
|
+
- No typos
|
|
231
|
+
|
|
232
|
+
See [MCP Prompts Documentation](../../docs/mcp-prompts.md).
|
|
233
|
+
|
|
105
234
|
## Core Tools
|
|
106
235
|
|
|
107
236
|
### smart_read
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-context-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
|
|
5
5
|
"author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision explainer - tracks and explains why devctx tools were used or not used
|
|
3
|
+
*
|
|
4
|
+
* Enable with environment variable: DEVCTX_EXPLAIN=true
|
|
5
|
+
*
|
|
6
|
+
* Provides transparency into agent decision-making:
|
|
7
|
+
* - Why was smart_read used instead of Read?
|
|
8
|
+
* - Why was smart_search chosen?
|
|
9
|
+
* - What are the expected benefits?
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const sessionDecisions = {
|
|
13
|
+
decisions: [],
|
|
14
|
+
enabled: false,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if explanations are enabled
|
|
19
|
+
*/
|
|
20
|
+
export const isExplainEnabled = () => {
|
|
21
|
+
const envValue = process.env.DEVCTX_EXPLAIN?.toLowerCase();
|
|
22
|
+
const enabled = envValue === 'true' || envValue === '1' || envValue === 'yes';
|
|
23
|
+
sessionDecisions.enabled = enabled;
|
|
24
|
+
return enabled;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Record a decision with explanation
|
|
29
|
+
*/
|
|
30
|
+
export const recordDecision = ({
|
|
31
|
+
tool,
|
|
32
|
+
action,
|
|
33
|
+
reason,
|
|
34
|
+
alternative = null,
|
|
35
|
+
expectedBenefit = null,
|
|
36
|
+
context = null,
|
|
37
|
+
}) => {
|
|
38
|
+
if (!isExplainEnabled()) return;
|
|
39
|
+
|
|
40
|
+
sessionDecisions.decisions.push({
|
|
41
|
+
tool,
|
|
42
|
+
action,
|
|
43
|
+
reason,
|
|
44
|
+
alternative,
|
|
45
|
+
expectedBenefit,
|
|
46
|
+
context,
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get all decisions for current session
|
|
53
|
+
*/
|
|
54
|
+
export const getSessionDecisions = () => {
|
|
55
|
+
return sessionDecisions.decisions;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format decisions as markdown for display
|
|
60
|
+
*/
|
|
61
|
+
export const formatDecisionExplanations = () => {
|
|
62
|
+
if (!isExplainEnabled()) return '';
|
|
63
|
+
|
|
64
|
+
const decisions = getSessionDecisions();
|
|
65
|
+
if (decisions.length === 0) return '';
|
|
66
|
+
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push('---');
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push('🤖 **Decision explanations:**');
|
|
72
|
+
lines.push('');
|
|
73
|
+
|
|
74
|
+
for (const decision of decisions) {
|
|
75
|
+
lines.push(`**${decision.tool}** (${decision.action})`);
|
|
76
|
+
lines.push(`- **Why:** ${decision.reason}`);
|
|
77
|
+
|
|
78
|
+
if (decision.alternative) {
|
|
79
|
+
lines.push(`- **Instead of:** ${decision.alternative}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (decision.expectedBenefit) {
|
|
83
|
+
lines.push(`- **Expected benefit:** ${decision.expectedBenefit}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (decision.context) {
|
|
87
|
+
lines.push(`- **Context:** ${decision.context}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lines.push('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lines.push('*To disable: `export DEVCTX_EXPLAIN=false`*');
|
|
94
|
+
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Reset session decisions (for testing or manual reset)
|
|
100
|
+
*/
|
|
101
|
+
export const resetSessionDecisions = () => {
|
|
102
|
+
sessionDecisions.decisions = [];
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Common decision reasons (for consistency)
|
|
107
|
+
*/
|
|
108
|
+
export const DECISION_REASONS = {
|
|
109
|
+
// smart_read reasons
|
|
110
|
+
LARGE_FILE: 'File is large (>500 lines), outline mode extracts structure only',
|
|
111
|
+
SYMBOL_EXTRACTION: 'Extracting specific symbol, smart_read can locate and extract it efficiently',
|
|
112
|
+
TOKEN_BUDGET: 'Token budget constraint, cascading to more compressed mode',
|
|
113
|
+
MULTIPLE_SYMBOLS: 'Reading multiple symbols, smart_read can batch them',
|
|
114
|
+
|
|
115
|
+
// smart_search reasons
|
|
116
|
+
MULTIPLE_FILES: 'Query spans 50+ files, smart_search ranks by relevance',
|
|
117
|
+
INTENT_AWARE: 'Intent-aware search prioritizes relevant results (debug/implementation/tests)',
|
|
118
|
+
INDEX_BOOST: 'Symbol index available, boosting relevant matches',
|
|
119
|
+
PATTERN_SEARCH: 'Complex pattern search, smart_search handles regex efficiently',
|
|
120
|
+
|
|
121
|
+
// smart_context reasons
|
|
122
|
+
TASK_CONTEXT: 'Building complete context for task, smart_context orchestrates multiple reads',
|
|
123
|
+
RELATED_FILES: 'Need related files (callers, tests, types), smart_context finds them',
|
|
124
|
+
ONE_CALL: 'Single call to get all context, more efficient than multiple reads',
|
|
125
|
+
DIFF_ANALYSIS: 'Analyzing git diff, smart_context expands changed symbols',
|
|
126
|
+
|
|
127
|
+
// smart_shell reasons
|
|
128
|
+
COMMAND_OUTPUT: 'Command output needs compression (git log, npm test, etc.)',
|
|
129
|
+
RELEVANT_LINES: 'Extracting relevant lines from command output',
|
|
130
|
+
SAFE_EXECUTION: 'Using allowlist-validated command execution',
|
|
131
|
+
|
|
132
|
+
// smart_summary reasons
|
|
133
|
+
CHECKPOINT: 'Saving task checkpoint for session recovery',
|
|
134
|
+
RESUME: 'Recovering previous task context',
|
|
135
|
+
PERSISTENCE: 'Maintaining task state across agent restarts',
|
|
136
|
+
|
|
137
|
+
// Native tool reasons
|
|
138
|
+
SIMPLE_TASK: 'Task is simple, native tool is more direct',
|
|
139
|
+
ALREADY_CACHED: 'Content already in context, no need for compression',
|
|
140
|
+
SINGLE_LINE: 'Reading single line, native Read is sufficient',
|
|
141
|
+
SMALL_FILE: 'File is small (<100 lines), compression not needed',
|
|
142
|
+
NO_INDEX: 'No symbol index available, native search is equivalent',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Common expected benefits (for consistency)
|
|
147
|
+
*/
|
|
148
|
+
export const EXPECTED_BENEFITS = {
|
|
149
|
+
TOKEN_SAVINGS: (tokens) => `~${formatTokens(tokens)} saved`,
|
|
150
|
+
FASTER_RESPONSE: 'Faster response due to less data to process',
|
|
151
|
+
BETTER_RANKING: 'Better result ranking, relevant items first',
|
|
152
|
+
COMPLETE_CONTEXT: 'Complete context in single call',
|
|
153
|
+
SESSION_RECOVERY: 'Can recover task state if agent restarts',
|
|
154
|
+
FOCUSED_RESULTS: 'Focused on relevant code only',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Format token count for display
|
|
159
|
+
*/
|
|
160
|
+
const formatTokens = (tokens) => {
|
|
161
|
+
if (tokens >= 1000000) {
|
|
162
|
+
return `${(tokens / 1000000).toFixed(1)}M tokens`;
|
|
163
|
+
}
|
|
164
|
+
if (tokens >= 1000) {
|
|
165
|
+
return `${(tokens / 1000).toFixed(1)}K tokens`;
|
|
166
|
+
}
|
|
167
|
+
return `${tokens} tokens`;
|
|
168
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Missed opportunities detector - identifies when devctx should have been used but wasn't
|
|
3
|
+
*
|
|
4
|
+
* Analyzes session metrics to detect patterns where devctx would have helped.
|
|
5
|
+
*
|
|
6
|
+
* Enable with environment variable: DEVCTX_DETECT_MISSED=true
|
|
7
|
+
*
|
|
8
|
+
* Detection heuristics (based on session metrics):
|
|
9
|
+
* - Low devctx adoption in complex sessions (many operations, few devctx calls)
|
|
10
|
+
* - Sessions with 0 devctx usage but high operation count
|
|
11
|
+
* - Inferred complexity vs actual devctx usage
|
|
12
|
+
*
|
|
13
|
+
* Note: We can't intercept native tool calls in real-time, so we analyze
|
|
14
|
+
* patterns from metrics after the fact.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const sessionActivity = {
|
|
18
|
+
devctxOperations: 0,
|
|
19
|
+
totalOperations: 0, // Estimated from devctx calls + time-based heuristic
|
|
20
|
+
lastDevctxCall: 0,
|
|
21
|
+
sessionStart: Date.now(),
|
|
22
|
+
enabled: false,
|
|
23
|
+
warnings: [],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEVCTX_TOOLS = new Set([
|
|
27
|
+
'smart_read',
|
|
28
|
+
'smart_search',
|
|
29
|
+
'smart_context',
|
|
30
|
+
'smart_shell',
|
|
31
|
+
'smart_summary',
|
|
32
|
+
'smart_turn',
|
|
33
|
+
'smart_read_batch',
|
|
34
|
+
'build_index',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if missed opportunity detection is enabled
|
|
39
|
+
*/
|
|
40
|
+
export const isMissedDetectionEnabled = () => {
|
|
41
|
+
const envValue = process.env.DEVCTX_DETECT_MISSED?.toLowerCase();
|
|
42
|
+
const enabled = envValue === 'true' || envValue === '1' || envValue === 'yes';
|
|
43
|
+
sessionActivity.enabled = enabled;
|
|
44
|
+
return enabled;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Record devctx tool usage
|
|
49
|
+
*/
|
|
50
|
+
export const recordDevctxOperation = () => {
|
|
51
|
+
if (!isMissedDetectionEnabled()) return;
|
|
52
|
+
|
|
53
|
+
sessionActivity.devctxOperations += 1;
|
|
54
|
+
sessionActivity.totalOperations += 1;
|
|
55
|
+
sessionActivity.lastDevctxCall = Date.now();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Estimate total operations based on time and activity
|
|
60
|
+
* Heuristic: If no devctx calls for >2 minutes, likely agent is using native tools
|
|
61
|
+
*/
|
|
62
|
+
const estimateTotalOperations = () => {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const sessionDuration = now - sessionActivity.sessionStart;
|
|
65
|
+
const timeSinceLastDevctx = now - sessionActivity.lastDevctxCall;
|
|
66
|
+
|
|
67
|
+
// If session is active (recent devctx calls), estimate conservatively
|
|
68
|
+
if (timeSinceLastDevctx < 2 * 60 * 1000) {
|
|
69
|
+
return sessionActivity.totalOperations;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If long gap without devctx, estimate agent is using native tools
|
|
73
|
+
// Heuristic: ~1 operation per 10 seconds of activity
|
|
74
|
+
const estimatedNativeOps = Math.floor(timeSinceLastDevctx / 10000);
|
|
75
|
+
return sessionActivity.totalOperations + estimatedNativeOps;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Analyze session and detect missed opportunities
|
|
80
|
+
*/
|
|
81
|
+
export const analyzeMissedOpportunities = () => {
|
|
82
|
+
if (!isMissedDetectionEnabled()) return null;
|
|
83
|
+
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const sessionDuration = now - sessionActivity.sessionStart;
|
|
86
|
+
const timeSinceLastDevctx = now - sessionActivity.lastDevctxCall;
|
|
87
|
+
const estimatedTotal = estimateTotalOperations();
|
|
88
|
+
|
|
89
|
+
const opportunities = [];
|
|
90
|
+
|
|
91
|
+
// Detection 1: Long session with no devctx usage
|
|
92
|
+
if (sessionDuration > 5 * 60 * 1000 && sessionActivity.devctxOperations === 0) {
|
|
93
|
+
opportunities.push({
|
|
94
|
+
type: 'no_devctx_usage',
|
|
95
|
+
severity: 'high',
|
|
96
|
+
reason: 'Session active for >5 minutes with 0 devctx calls. Agent may not be using devctx.',
|
|
97
|
+
suggestion: 'Use forcing prompt or check if MCP is active',
|
|
98
|
+
estimatedSavings: estimatedTotal * 10000, // Estimate ~10K tokens per operation
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Detection 2: Low devctx adoption in active session
|
|
103
|
+
const devctxRatio = estimatedTotal > 0 ? sessionActivity.devctxOperations / estimatedTotal : 0;
|
|
104
|
+
if (estimatedTotal >= 10 && devctxRatio < 0.3) {
|
|
105
|
+
opportunities.push({
|
|
106
|
+
type: 'low_devctx_adoption',
|
|
107
|
+
severity: 'medium',
|
|
108
|
+
reason: `Low devctx adoption: ${sessionActivity.devctxOperations}/${estimatedTotal} operations (${Math.round(devctxRatio * 100)}%). Target: >50%.`,
|
|
109
|
+
suggestion: 'Agent may be using native tools. Consider forcing prompt.',
|
|
110
|
+
estimatedSavings: (estimatedTotal - sessionActivity.devctxOperations) * 8000,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Detection 3: Long gap without devctx (agent switched to native tools)
|
|
115
|
+
if (sessionActivity.devctxOperations > 0 && timeSinceLastDevctx > 3 * 60 * 1000) {
|
|
116
|
+
const minutesSince = Math.round(timeSinceLastDevctx / 60000);
|
|
117
|
+
opportunities.push({
|
|
118
|
+
type: 'devctx_usage_dropped',
|
|
119
|
+
severity: 'medium',
|
|
120
|
+
reason: `No devctx calls for ${minutesSince} minutes. Agent may have switched to native tools.`,
|
|
121
|
+
suggestion: 'Re-apply forcing prompt if task is still complex',
|
|
122
|
+
estimatedSavings: Math.floor(timeSinceLastDevctx / 10000) * 5000,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Detection 4: Session too short to analyze
|
|
127
|
+
if (sessionDuration < 60 * 1000 && opportunities.length === 0) {
|
|
128
|
+
return {
|
|
129
|
+
opportunities: [],
|
|
130
|
+
message: 'Session too short to analyze (<1 minute)',
|
|
131
|
+
devctxOperations: sessionActivity.devctxOperations,
|
|
132
|
+
estimatedTotal,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
opportunities,
|
|
138
|
+
devctxOperations: sessionActivity.devctxOperations,
|
|
139
|
+
estimatedTotal,
|
|
140
|
+
devctxRatio: Math.round(devctxRatio * 100),
|
|
141
|
+
sessionDuration: Math.round(sessionDuration / 1000),
|
|
142
|
+
totalEstimatedSavings: opportunities.reduce((sum, opp) => sum + (opp.estimatedSavings || 0), 0),
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Format missed opportunities as markdown
|
|
148
|
+
*/
|
|
149
|
+
export const formatMissedOpportunities = () => {
|
|
150
|
+
if (!isMissedDetectionEnabled()) return '';
|
|
151
|
+
|
|
152
|
+
const analysis = analyzeMissedOpportunities();
|
|
153
|
+
if (!analysis) return '';
|
|
154
|
+
|
|
155
|
+
// Don't show if session is too short or no opportunities
|
|
156
|
+
if (analysis.message || analysis.opportunities.length === 0) {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const lines = [];
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push('---');
|
|
163
|
+
lines.push('');
|
|
164
|
+
lines.push('⚠️ **Missed devctx opportunities detected:**');
|
|
165
|
+
lines.push('');
|
|
166
|
+
|
|
167
|
+
// Show session stats
|
|
168
|
+
lines.push(`**Session stats:**`);
|
|
169
|
+
lines.push(`- Duration: ${analysis.sessionDuration}s`);
|
|
170
|
+
lines.push(`- devctx operations: ${analysis.devctxOperations}`);
|
|
171
|
+
lines.push(`- Estimated total operations: ${analysis.estimatedTotal}`);
|
|
172
|
+
lines.push(`- devctx adoption: ${analysis.devctxRatio}%`);
|
|
173
|
+
lines.push('');
|
|
174
|
+
|
|
175
|
+
for (const opp of analysis.opportunities) {
|
|
176
|
+
const severityIcon = opp.severity === 'high' ? '🔴' : '🟡';
|
|
177
|
+
lines.push(`${severityIcon} **${opp.type.replace(/_/g, ' ')}**`);
|
|
178
|
+
lines.push(`- **Issue:** ${opp.reason}`);
|
|
179
|
+
lines.push(`- **Suggestion:** ${opp.suggestion}`);
|
|
180
|
+
|
|
181
|
+
if (opp.estimatedSavings) {
|
|
182
|
+
lines.push(`- **Potential savings:** ~${formatTokens(opp.estimatedSavings)}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lines.push('');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (analysis.totalEstimatedSavings > 0) {
|
|
189
|
+
lines.push(`**Total potential savings:** ~${formatTokens(analysis.totalEstimatedSavings)}`);
|
|
190
|
+
lines.push('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
lines.push('**How to fix:**');
|
|
194
|
+
lines.push('1. Use forcing prompt: `Use devctx: smart_turn(start) → smart_context/smart_search → smart_read → smart_turn(end)`');
|
|
195
|
+
lines.push('2. Check if index is built: `ls .devctx/index.json`');
|
|
196
|
+
lines.push('3. Verify MCP is active in Cursor settings');
|
|
197
|
+
lines.push('');
|
|
198
|
+
lines.push('*To disable: `export DEVCTX_DETECT_MISSED=false`*');
|
|
199
|
+
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get session activity summary
|
|
205
|
+
*/
|
|
206
|
+
export const getSessionActivity = () => {
|
|
207
|
+
return {
|
|
208
|
+
devctxOperations: sessionActivity.devctxOperations,
|
|
209
|
+
totalOperations: sessionActivity.totalOperations,
|
|
210
|
+
estimatedTotal: estimateTotalOperations(),
|
|
211
|
+
sessionDuration: Date.now() - sessionActivity.sessionStart,
|
|
212
|
+
timeSinceLastDevctx: Date.now() - sessionActivity.lastDevctxCall,
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Reset session activity (for testing or manual reset)
|
|
218
|
+
*/
|
|
219
|
+
export const resetSessionActivity = () => {
|
|
220
|
+
sessionActivity.devctxOperations = 0;
|
|
221
|
+
sessionActivity.totalOperations = 0;
|
|
222
|
+
sessionActivity.lastDevctxCall = 0;
|
|
223
|
+
sessionActivity.sessionStart = Date.now();
|
|
224
|
+
sessionActivity.warnings = [];
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Testing helpers - expose internal state for test scenarios
|
|
229
|
+
*/
|
|
230
|
+
export const __testing__ = {
|
|
231
|
+
setSessionStart: (timestamp) => {
|
|
232
|
+
sessionActivity.sessionStart = timestamp;
|
|
233
|
+
},
|
|
234
|
+
setLastDevctxCall: (timestamp) => {
|
|
235
|
+
sessionActivity.lastDevctxCall = timestamp;
|
|
236
|
+
},
|
|
237
|
+
setTotalOperations: (count) => {
|
|
238
|
+
sessionActivity.totalOperations = count;
|
|
239
|
+
},
|
|
240
|
+
getSessionActivity: () => sessionActivity,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Format token count for display
|
|
245
|
+
*/
|
|
246
|
+
const formatTokens = (tokens) => {
|
|
247
|
+
if (tokens >= 1000000) {
|
|
248
|
+
return `${(tokens / 1000000).toFixed(1)}M tokens`;
|
|
249
|
+
}
|
|
250
|
+
if (tokens >= 1000) {
|
|
251
|
+
return `${(tokens / 1000).toFixed(1)}K tokens`;
|
|
252
|
+
}
|
|
253
|
+
return `${tokens} tokens`;
|
|
254
|
+
};
|
|
255
|
+
|
package/src/server.js
CHANGED
|
@@ -49,6 +49,72 @@ export const createDevctxServer = () => {
|
|
|
49
49
|
// Enable streaming progress notifications
|
|
50
50
|
setServerForStreaming(server);
|
|
51
51
|
|
|
52
|
+
// Register prompts
|
|
53
|
+
server.prompt(
|
|
54
|
+
'use-devctx',
|
|
55
|
+
'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.',
|
|
56
|
+
{},
|
|
57
|
+
async () => ({
|
|
58
|
+
messages: [
|
|
59
|
+
{
|
|
60
|
+
role: 'user',
|
|
61
|
+
content: {
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: 'Use devctx: smart_turn(start) → smart_context/smart_search → smart_read → smart_turn(end)',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
server.prompt(
|
|
71
|
+
'devctx-workflow',
|
|
72
|
+
'Complete devctx workflow template with all recommended steps. Includes session start, context building, file reading, and session end.',
|
|
73
|
+
{},
|
|
74
|
+
async () => ({
|
|
75
|
+
messages: [
|
|
76
|
+
{
|
|
77
|
+
role: 'user',
|
|
78
|
+
content: {
|
|
79
|
+
type: 'text',
|
|
80
|
+
text: `Follow this devctx workflow:
|
|
81
|
+
|
|
82
|
+
1. smart_turn(start) - Start session and recover previous context
|
|
83
|
+
2. smart_context(task) - Build complete context for the task
|
|
84
|
+
3. smart_search(query) - Search for specific patterns if needed
|
|
85
|
+
4. smart_read(file) - Read files with appropriate mode (outline/signatures/symbol)
|
|
86
|
+
5. smart_turn(end) - Save checkpoint for next session
|
|
87
|
+
|
|
88
|
+
Use devctx tools instead of native Read/Grep/Shell when possible.`,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
server.prompt(
|
|
96
|
+
'devctx-preflight',
|
|
97
|
+
'Preflight checklist before starting work. Ensures index is built and session is initialized.',
|
|
98
|
+
{},
|
|
99
|
+
async () => ({
|
|
100
|
+
messages: [
|
|
101
|
+
{
|
|
102
|
+
role: 'user',
|
|
103
|
+
content: {
|
|
104
|
+
type: 'text',
|
|
105
|
+
text: `Preflight checklist:
|
|
106
|
+
|
|
107
|
+
1. build_index(incremental=true) - Build/update symbol index
|
|
108
|
+
2. smart_turn(start) - Initialize session and recover context
|
|
109
|
+
3. Proceed with your task using devctx tools
|
|
110
|
+
|
|
111
|
+
This ensures optimal performance and context recovery.`,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
|
|
52
118
|
server.tool(
|
|
53
119
|
'smart_read',
|
|
54
120
|
'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.',
|
|
@@ -11,6 +11,9 @@ import { resolveSafePath } from '../utils/fs.js';
|
|
|
11
11
|
import { countTokens } from '../tokenCounter.js';
|
|
12
12
|
import { persistMetrics } from '../metrics.js';
|
|
13
13
|
import { predictContextFiles, recordContextAccess } from '../context-patterns.js';
|
|
14
|
+
import { recordToolUsage } from '../usage-feedback.js';
|
|
15
|
+
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
16
|
+
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
14
17
|
import {
|
|
15
18
|
getDetailedDiff,
|
|
16
19
|
analyzeChangeImpact,
|
|
@@ -1219,15 +1222,44 @@ export const smartContext = async ({
|
|
|
1219
1222
|
const contentItems = context.filter((item) => typeof item.content === 'string' && item.content.length > 0).length;
|
|
1220
1223
|
const primaryItem = context.find((item) => item.role === 'primary');
|
|
1221
1224
|
|
|
1225
|
+
const savedTokens = Math.max(0, totalRawTokens - totalCompressedTokens);
|
|
1226
|
+
|
|
1222
1227
|
await persistMetrics({
|
|
1223
1228
|
tool: 'smart_context',
|
|
1224
1229
|
target: `${root} :: ${task}`,
|
|
1225
1230
|
rawTokens: totalRawTokens,
|
|
1226
1231
|
compressedTokens: totalCompressedTokens,
|
|
1227
|
-
savedTokens
|
|
1232
|
+
savedTokens,
|
|
1228
1233
|
savingsPct,
|
|
1229
1234
|
timestamp: new Date().toISOString(),
|
|
1230
1235
|
});
|
|
1236
|
+
|
|
1237
|
+
// Record usage for feedback
|
|
1238
|
+
recordToolUsage({
|
|
1239
|
+
tool: 'smart_context',
|
|
1240
|
+
savedTokens,
|
|
1241
|
+
target: task,
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
// Record devctx operation for missed opportunity detection
|
|
1245
|
+
recordDevctxOperation();
|
|
1246
|
+
|
|
1247
|
+
// Record decision explanation
|
|
1248
|
+
let reason = DECISION_REASONS.TASK_CONTEXT;
|
|
1249
|
+
if (diff) {
|
|
1250
|
+
reason = DECISION_REASONS.DIFF_ANALYSIS;
|
|
1251
|
+
} else if (context.some(c => c.role === 'caller' || c.role === 'test')) {
|
|
1252
|
+
reason = DECISION_REASONS.RELATED_FILES;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
recordDecision({
|
|
1256
|
+
tool: 'smart_context',
|
|
1257
|
+
action: `build context for "${task}"`,
|
|
1258
|
+
reason,
|
|
1259
|
+
alternative: 'Multiple smart_read + smart_search calls',
|
|
1260
|
+
expectedBenefit: `${EXPECTED_BENEFITS.TOKEN_SAVINGS(savedTokens)}, ${EXPECTED_BENEFITS.COMPLETE_CONTEXT}`,
|
|
1261
|
+
context: `${context.length} files, ${totalCompressedTokens} tokens (${savingsPct}% compression)`,
|
|
1262
|
+
});
|
|
1231
1263
|
|
|
1232
1264
|
if (prefetch && context.length > 0) {
|
|
1233
1265
|
try {
|
package/src/tools/smart-read.js
CHANGED
|
@@ -9,6 +9,9 @@ import { isDockerfile, readTextFile } from '../utils/fs.js';
|
|
|
9
9
|
import { projectRoot } from '../utils/paths.js';
|
|
10
10
|
import { truncate } from '../utils/text.js';
|
|
11
11
|
import { countTokens } from '../tokenCounter.js';
|
|
12
|
+
import { recordToolUsage } from '../usage-feedback.js';
|
|
13
|
+
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
14
|
+
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
12
15
|
|
|
13
16
|
const execFile = promisify(execFileCb);
|
|
14
17
|
import { summarizeGo, summarizeRust, summarizeJava, summarizeShell, summarizeTerraform, summarizeDockerfile, summarizeSql, extractGoSymbol, extractRustSymbol, extractJavaSymbol, summarizeCsharp, extractCsharpSymbol, summarizeKotlin, extractKotlinSymbol, summarizePhp, extractPhpSymbol, summarizeSwift, extractSwiftSymbol } from './smart-read/additional-languages.js';
|
|
@@ -453,6 +456,38 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
453
456
|
});
|
|
454
457
|
|
|
455
458
|
await persistMetrics(metrics);
|
|
459
|
+
|
|
460
|
+
// Record usage for feedback
|
|
461
|
+
recordToolUsage({
|
|
462
|
+
tool: 'smart_read',
|
|
463
|
+
savedTokens: metrics.savedTokens,
|
|
464
|
+
target: path.relative(projectRoot, fullPath),
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Record devctx operation for missed opportunity detection
|
|
468
|
+
recordDevctxOperation();
|
|
469
|
+
|
|
470
|
+
// Record decision explanation
|
|
471
|
+
const lineCount = content.split('\n').length;
|
|
472
|
+
let reason = DECISION_REASONS.LARGE_FILE;
|
|
473
|
+
let expectedBenefit = EXPECTED_BENEFITS.TOKEN_SAVINGS(metrics.savedTokens);
|
|
474
|
+
|
|
475
|
+
if (mode === 'symbol') {
|
|
476
|
+
reason = DECISION_REASONS.SYMBOL_EXTRACTION;
|
|
477
|
+
} else if (validBudget && effectiveMode !== mode) {
|
|
478
|
+
reason = DECISION_REASONS.TOKEN_BUDGET;
|
|
479
|
+
} else if (lineCount < 100) {
|
|
480
|
+
reason = `File is small (${lineCount} lines), but using ${effectiveMode} mode for consistency`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
recordDecision({
|
|
484
|
+
tool: 'smart_read',
|
|
485
|
+
action: `read ${path.relative(projectRoot, fullPath)} (${effectiveMode} mode)`,
|
|
486
|
+
reason,
|
|
487
|
+
alternative: 'Read (full file)',
|
|
488
|
+
expectedBenefit,
|
|
489
|
+
context: `${lineCount} lines, ${metrics.rawTokens} tokens → ${metrics.compressedTokens} tokens`,
|
|
490
|
+
});
|
|
456
491
|
|
|
457
492
|
const confidence = { parser, truncated, cached: cacheHit && !contextResult };
|
|
458
493
|
if (contextResult) confidence.graphCoverage = contextResult.graphCoverage;
|
|
@@ -8,6 +8,9 @@ import { loadIndex, queryIndex, queryRelated } from '../index.js';
|
|
|
8
8
|
import { projectRoot } from '../utils/paths.js';
|
|
9
9
|
import { isBinaryBuffer, isDockerfile, resolveSafePath } from '../utils/fs.js';
|
|
10
10
|
import { truncate } from '../utils/text.js';
|
|
11
|
+
import { recordToolUsage } from '../usage-feedback.js';
|
|
12
|
+
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
13
|
+
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
11
14
|
|
|
12
15
|
const execFile = promisify(execFileCallback);
|
|
13
16
|
const supportedGlobs = [
|
|
@@ -381,6 +384,34 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
381
384
|
});
|
|
382
385
|
|
|
383
386
|
await persistMetrics(metrics);
|
|
387
|
+
|
|
388
|
+
// Record usage for feedback
|
|
389
|
+
recordToolUsage({
|
|
390
|
+
tool: 'smart_search',
|
|
391
|
+
savedTokens: metrics.savedTokens,
|
|
392
|
+
target: query,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Record devctx operation for missed opportunity detection
|
|
396
|
+
recordDevctxOperation();
|
|
397
|
+
|
|
398
|
+
// Record decision explanation
|
|
399
|
+
let reason = DECISION_REASONS.MULTIPLE_FILES;
|
|
400
|
+
if (validIntent) {
|
|
401
|
+
reason = DECISION_REASONS.INTENT_AWARE;
|
|
402
|
+
}
|
|
403
|
+
if (indexHits && indexHits.size > 0) {
|
|
404
|
+
reason = DECISION_REASONS.INDEX_BOOST;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
recordDecision({
|
|
408
|
+
tool: 'smart_search',
|
|
409
|
+
action: `search "${query}"${validIntent ? ` (intent: ${validIntent})` : ''}`,
|
|
410
|
+
reason,
|
|
411
|
+
alternative: 'Grep (unranked results)',
|
|
412
|
+
expectedBenefit: `${EXPECTED_BENEFITS.TOKEN_SAVINGS(metrics.savedTokens)}, ${EXPECTED_BENEFITS.BETTER_RANKING}`,
|
|
413
|
+
context: `${dedupedMatches.length} matches in ${groups.length} files, ranked by relevance`,
|
|
414
|
+
});
|
|
384
415
|
|
|
385
416
|
let retrievalConfidence = 'high';
|
|
386
417
|
if (provenance) {
|
package/src/tools/smart-shell.js
CHANGED
|
@@ -4,6 +4,9 @@ import { rgPath } from '@vscode/ripgrep';
|
|
|
4
4
|
import { buildMetrics, persistMetrics } from '../metrics.js';
|
|
5
5
|
import { projectRoot } from '../utils/paths.js';
|
|
6
6
|
import { pickRelevantLines, truncate, uniqueLines } from '../utils/text.js';
|
|
7
|
+
import { recordToolUsage } from '../usage-feedback.js';
|
|
8
|
+
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
9
|
+
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
7
10
|
|
|
8
11
|
const execFile = promisify(execFileCallback);
|
|
9
12
|
const isShellDisabled = () => process.env.DEVCTX_SHELL_DISABLED === 'true';
|
|
@@ -221,6 +224,32 @@ export const smartShell = async ({ command }) => {
|
|
|
221
224
|
});
|
|
222
225
|
|
|
223
226
|
await persistMetrics(metrics);
|
|
227
|
+
|
|
228
|
+
// Record usage for feedback
|
|
229
|
+
recordToolUsage({
|
|
230
|
+
tool: 'smart_shell',
|
|
231
|
+
savedTokens: metrics.savedTokens,
|
|
232
|
+
target: command,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Record devctx operation for missed opportunity detection
|
|
236
|
+
recordDevctxOperation();
|
|
237
|
+
|
|
238
|
+
// Record decision explanation
|
|
239
|
+
const outputLines = rawText.split('\n').length;
|
|
240
|
+
let reason = DECISION_REASONS.COMMAND_OUTPUT;
|
|
241
|
+
if (shouldPrioritizeRelevant && relevant) {
|
|
242
|
+
reason = DECISION_REASONS.RELEVANT_LINES;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
recordDecision({
|
|
246
|
+
tool: 'smart_shell',
|
|
247
|
+
action: `execute "${command}"`,
|
|
248
|
+
reason,
|
|
249
|
+
alternative: 'Shell (uncompressed output)',
|
|
250
|
+
expectedBenefit: EXPECTED_BENEFITS.TOKEN_SAVINGS(metrics.savedTokens),
|
|
251
|
+
context: `${outputLines} lines → ${compressedText.split('\n').length} lines (relevant only)`,
|
|
252
|
+
});
|
|
224
253
|
|
|
225
254
|
const result = {
|
|
226
255
|
command,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { countTokens } from '../tokenCounter.js';
|
|
2
2
|
import { persistMetrics } from '../metrics.js';
|
|
3
3
|
import { enforceRepoSafety, getRepoSafety } from '../repo-safety.js';
|
|
4
|
+
import { recordToolUsage } from '../usage-feedback.js';
|
|
5
|
+
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
6
|
+
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
4
7
|
import {
|
|
5
8
|
ACTIVE_SESSION_SCOPE,
|
|
6
9
|
SQLITE_SCHEMA_VERSION,
|
|
@@ -1212,6 +1215,23 @@ export const smartSummary = async ({
|
|
|
1212
1215
|
...summaryMetrics,
|
|
1213
1216
|
latencyMs: Date.now() - startTime,
|
|
1214
1217
|
});
|
|
1218
|
+
|
|
1219
|
+
recordToolUsage({
|
|
1220
|
+
tool: 'smart_summary',
|
|
1221
|
+
savedTokens: summaryMetrics.savedTokens || 0,
|
|
1222
|
+
target: targetSessionId,
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
recordDevctxOperation();
|
|
1226
|
+
|
|
1227
|
+
recordDecision({
|
|
1228
|
+
tool: 'smart_summary',
|
|
1229
|
+
action: `get checkpoint "${targetSessionId}"`,
|
|
1230
|
+
reason: DECISION_REASONS.RESUME,
|
|
1231
|
+
alternative: 'Start from scratch (lose context)',
|
|
1232
|
+
expectedBenefit: EXPECTED_BENEFITS.SESSION_RECOVERY,
|
|
1233
|
+
context: `Recovered ${compressed.goal ? 'goal' : 'state'}, ${compressed.status || 'unknown'} status`,
|
|
1234
|
+
});
|
|
1215
1235
|
}
|
|
1216
1236
|
|
|
1217
1237
|
return addRepoSafety({
|
|
@@ -1356,14 +1376,21 @@ export const smartSummary = async ({
|
|
|
1356
1376
|
const { compressed, tokens, truncated, omitted, compressionLevel } = compressSummary(currentSession, maxTokens);
|
|
1357
1377
|
const rawTokens = countTokens(JSON.stringify(currentSession));
|
|
1358
1378
|
|
|
1379
|
+
const metrics = buildSummaryMetrics(rawTokens, tokens);
|
|
1359
1380
|
persistMetrics({
|
|
1360
1381
|
tool: 'smart_summary',
|
|
1361
1382
|
action,
|
|
1362
1383
|
sessionId: targetSessionId,
|
|
1363
|
-
...
|
|
1384
|
+
...metrics,
|
|
1364
1385
|
latencyMs: Date.now() - startTime,
|
|
1365
1386
|
skipped: true,
|
|
1366
1387
|
});
|
|
1388
|
+
|
|
1389
|
+
recordToolUsage({
|
|
1390
|
+
tool: 'smart_summary',
|
|
1391
|
+
savedTokens: metrics.savedTokens || 0,
|
|
1392
|
+
target: targetSessionId,
|
|
1393
|
+
});
|
|
1367
1394
|
|
|
1368
1395
|
return addRepoSafety({
|
|
1369
1396
|
action,
|
|
@@ -1386,15 +1413,24 @@ export const smartSummary = async ({
|
|
|
1386
1413
|
const { compressed, tokens, truncated, omitted, compressionLevel } = compressSummary(currentSession, maxTokens);
|
|
1387
1414
|
const rawTokens = countTokens(JSON.stringify(currentSession));
|
|
1388
1415
|
|
|
1416
|
+
const metrics2 = buildSummaryMetrics(rawTokens, tokens);
|
|
1389
1417
|
persistMetrics({
|
|
1390
1418
|
tool: 'smart_summary',
|
|
1391
1419
|
action,
|
|
1392
1420
|
sessionId: targetSessionId,
|
|
1393
|
-
...
|
|
1421
|
+
...metrics2,
|
|
1394
1422
|
latencyMs: Date.now() - startTime,
|
|
1395
1423
|
skipped: true,
|
|
1396
1424
|
checkpointEvent: checkpointDecision.event,
|
|
1397
1425
|
});
|
|
1426
|
+
|
|
1427
|
+
recordToolUsage({
|
|
1428
|
+
tool: 'smart_summary',
|
|
1429
|
+
savedTokens: metrics2.savedTokens || 0,
|
|
1430
|
+
target: targetSessionId,
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
recordDevctxOperation();
|
|
1398
1434
|
|
|
1399
1435
|
return addRepoSafety({
|
|
1400
1436
|
action,
|
|
@@ -1455,6 +1491,14 @@ export const smartSummary = async ({
|
|
|
1455
1491
|
latencyMs: Date.now() - startTime,
|
|
1456
1492
|
...(action === 'checkpoint' ? { checkpointEvent: checkpointDecision.event } : {}),
|
|
1457
1493
|
});
|
|
1494
|
+
|
|
1495
|
+
recordToolUsage({
|
|
1496
|
+
tool: 'smart_summary',
|
|
1497
|
+
savedTokens: summaryMetrics.savedTokens || 0,
|
|
1498
|
+
target: targetSessionId,
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
recordDevctxOperation();
|
|
1458
1502
|
|
|
1459
1503
|
return addRepoSafety({
|
|
1460
1504
|
action,
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage feedback system - tracks devctx tool usage in current session
|
|
3
|
+
* and provides visible feedback to users about what tools were used and tokens saved.
|
|
4
|
+
*
|
|
5
|
+
* Enable with environment variable: DEVCTX_SHOW_USAGE=true
|
|
6
|
+
*
|
|
7
|
+
* Auto-enabled for first 10 tool calls (onboarding mode), then auto-disables.
|
|
8
|
+
* User can explicitly enable/disable at any time.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const sessionUsage = {
|
|
12
|
+
tools: new Map(), // toolName -> { count, savedTokens }
|
|
13
|
+
totalSavedTokens: 0,
|
|
14
|
+
enabled: false,
|
|
15
|
+
totalToolCalls: 0,
|
|
16
|
+
onboardingMode: true,
|
|
17
|
+
ONBOARDING_THRESHOLD: 10, // Auto-disable after 10 tool calls
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if usage feedback is enabled
|
|
22
|
+
*
|
|
23
|
+
* Priority:
|
|
24
|
+
* 1. Explicit env var (DEVCTX_SHOW_USAGE=true/false)
|
|
25
|
+
* 2. Onboarding mode (first 10 tool calls)
|
|
26
|
+
* 3. Default: disabled
|
|
27
|
+
*/
|
|
28
|
+
export const isFeedbackEnabled = () => {
|
|
29
|
+
const envValue = process.env.DEVCTX_SHOW_USAGE?.toLowerCase();
|
|
30
|
+
|
|
31
|
+
// Explicit enable
|
|
32
|
+
if (envValue === 'true' || envValue === '1' || envValue === 'yes') {
|
|
33
|
+
sessionUsage.enabled = true;
|
|
34
|
+
sessionUsage.onboardingMode = false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Explicit disable
|
|
39
|
+
if (envValue === 'false' || envValue === '0' || envValue === 'no') {
|
|
40
|
+
sessionUsage.enabled = false;
|
|
41
|
+
sessionUsage.onboardingMode = false;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Onboarding mode: auto-enable for first N tool calls
|
|
46
|
+
if (sessionUsage.onboardingMode && sessionUsage.totalToolCalls < sessionUsage.ONBOARDING_THRESHOLD) {
|
|
47
|
+
sessionUsage.enabled = true;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// After onboarding threshold, auto-disable
|
|
52
|
+
if (sessionUsage.onboardingMode && sessionUsage.totalToolCalls >= sessionUsage.ONBOARDING_THRESHOLD) {
|
|
53
|
+
sessionUsage.enabled = false;
|
|
54
|
+
sessionUsage.onboardingMode = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return sessionUsage.enabled;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record tool usage for feedback
|
|
62
|
+
*/
|
|
63
|
+
export const recordToolUsage = ({ tool, savedTokens = 0, target = null }) => {
|
|
64
|
+
// Increment total tool calls (for onboarding mode)
|
|
65
|
+
sessionUsage.totalToolCalls += 1;
|
|
66
|
+
|
|
67
|
+
if (!isFeedbackEnabled()) return;
|
|
68
|
+
|
|
69
|
+
const current = sessionUsage.tools.get(tool) || { count: 0, savedTokens: 0, targets: [] };
|
|
70
|
+
current.count += 1;
|
|
71
|
+
current.savedTokens += savedTokens;
|
|
72
|
+
if (target) current.targets.push(target);
|
|
73
|
+
|
|
74
|
+
sessionUsage.tools.set(tool, current);
|
|
75
|
+
sessionUsage.totalSavedTokens += savedTokens;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get current session usage stats
|
|
80
|
+
*/
|
|
81
|
+
export const getSessionUsage = () => {
|
|
82
|
+
return {
|
|
83
|
+
tools: Array.from(sessionUsage.tools.entries()).map(([tool, stats]) => ({
|
|
84
|
+
tool,
|
|
85
|
+
count: stats.count,
|
|
86
|
+
savedTokens: stats.savedTokens,
|
|
87
|
+
targets: stats.targets.slice(-3), // Last 3 targets only
|
|
88
|
+
})),
|
|
89
|
+
totalSavedTokens: sessionUsage.totalSavedTokens,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format usage feedback as markdown
|
|
95
|
+
*/
|
|
96
|
+
export const formatUsageFeedback = () => {
|
|
97
|
+
if (!isFeedbackEnabled()) return '';
|
|
98
|
+
|
|
99
|
+
const usage = getSessionUsage();
|
|
100
|
+
if (usage.tools.length === 0) return '';
|
|
101
|
+
|
|
102
|
+
const lines = [];
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push('---');
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push('📊 **devctx usage this session:**');
|
|
107
|
+
|
|
108
|
+
// Sort by count descending
|
|
109
|
+
const sorted = usage.tools.sort((a, b) => b.count - a.count);
|
|
110
|
+
|
|
111
|
+
for (const { tool, count, savedTokens, targets } of sorted) {
|
|
112
|
+
const countStr = count === 1 ? '1 call' : `${count} calls`;
|
|
113
|
+
const tokensStr = savedTokens > 0 ? ` | ~${formatTokens(savedTokens)} saved` : '';
|
|
114
|
+
|
|
115
|
+
if (targets.length > 0) {
|
|
116
|
+
const targetsPreview = targets.length === 1
|
|
117
|
+
? ` (${truncateTarget(targets[0])})`
|
|
118
|
+
: ` (${targets.length} files)`;
|
|
119
|
+
lines.push(`- **${tool}**: ${countStr}${tokensStr}${targetsPreview}`);
|
|
120
|
+
} else {
|
|
121
|
+
lines.push(`- **${tool}**: ${countStr}${tokensStr}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (usage.totalSavedTokens > 0) {
|
|
126
|
+
lines.push('');
|
|
127
|
+
lines.push(`**Total saved:** ~${formatTokens(usage.totalSavedTokens)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
lines.push('');
|
|
131
|
+
|
|
132
|
+
// Show onboarding message if in onboarding mode
|
|
133
|
+
if (sessionUsage.onboardingMode && sessionUsage.totalToolCalls < sessionUsage.ONBOARDING_THRESHOLD) {
|
|
134
|
+
const remaining = sessionUsage.ONBOARDING_THRESHOLD - sessionUsage.totalToolCalls;
|
|
135
|
+
lines.push(`*Onboarding mode: showing for ${remaining} more tool calls. To keep: \`export DEVCTX_SHOW_USAGE=true\`*`);
|
|
136
|
+
} else {
|
|
137
|
+
lines.push('*To disable this message: `export DEVCTX_SHOW_USAGE=false`*');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return lines.join('\n');
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Reset session usage (for testing or manual reset)
|
|
145
|
+
*/
|
|
146
|
+
export const resetSessionUsage = () => {
|
|
147
|
+
sessionUsage.tools.clear();
|
|
148
|
+
sessionUsage.totalSavedTokens = 0;
|
|
149
|
+
sessionUsage.totalToolCalls = 0;
|
|
150
|
+
sessionUsage.onboardingMode = true;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Format token count for display
|
|
155
|
+
*/
|
|
156
|
+
const formatTokens = (tokens) => {
|
|
157
|
+
if (tokens >= 1000000) {
|
|
158
|
+
return `${(tokens / 1000000).toFixed(1)}M tokens`;
|
|
159
|
+
}
|
|
160
|
+
if (tokens >= 1000) {
|
|
161
|
+
return `${(tokens / 1000).toFixed(1)}K tokens`;
|
|
162
|
+
}
|
|
163
|
+
return `${tokens} tokens`;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Truncate target path for display
|
|
168
|
+
*/
|
|
169
|
+
const truncateTarget = (target) => {
|
|
170
|
+
if (!target) return '';
|
|
171
|
+
if (target.length <= 40) return target;
|
|
172
|
+
|
|
173
|
+
// Try to show filename
|
|
174
|
+
const parts = target.split('/');
|
|
175
|
+
const filename = parts[parts.length - 1];
|
|
176
|
+
if (filename.length <= 40) return `.../${filename}`;
|
|
177
|
+
|
|
178
|
+
return target.slice(0, 37) + '...';
|
|
179
|
+
};
|