smart-context-mcp 1.1.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.
@@ -4,13 +4,26 @@ 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
- const blockedPattern = /[|&;<>`\n\r]/;
12
+ const isShellDisabled = () => process.env.DEVCTX_SHELL_DISABLED === 'true';
13
+ const blockedPattern = /[|&;<>`\n\r$()]/;
10
14
  const allowedCommands = new Set(['pwd', 'ls', 'find', 'rg', 'git', 'npm', 'pnpm', 'yarn', 'bun']);
11
- const allowedGitSubcommands = new Set(['status', 'diff', 'show', 'log', 'branch', 'rev-parse']);
15
+ const allowedGitSubcommands = new Set(['status', 'diff', 'show', 'log', 'branch', 'rev-parse', 'blame']);
12
16
  const allowedPackageManagerSubcommands = new Set(['test', 'run', 'lint', 'build', 'typecheck', 'check']);
13
- const safeRunScriptPattern = /^(test|lint|build|typecheck|check|smoke|verify)(:|$)/;
17
+ const safeRunScriptPattern = /^(test|lint|build|typecheck|check|smoke|verify|eval)(:|$)/;
18
+ const dangerousPatterns = [
19
+ /rm\s+-rf/i,
20
+ /sudo/i,
21
+ /curl.*\|/i,
22
+ /wget.*\|/i,
23
+ /eval/i,
24
+ /exec/i,
25
+ ];
26
+ const MAX_COMMAND_LENGTH = 500;
14
27
 
15
28
  const tokenize = (command) => {
16
29
  const tokens = [];
@@ -67,12 +80,26 @@ const tokenize = (command) => {
67
80
  };
68
81
 
69
82
  const validateCommand = (command, tokens) => {
83
+ if (isShellDisabled()) {
84
+ return 'Shell execution is disabled (DEVCTX_SHELL_DISABLED=true)';
85
+ }
86
+
70
87
  if (!command.trim()) {
71
88
  return 'Command is empty';
72
89
  }
73
90
 
74
- if (blockedPattern.test(command) || command.includes('$(')) {
75
- return 'Shell operators are not allowed';
91
+ if (command.length > MAX_COMMAND_LENGTH) {
92
+ return `Command too long (max ${MAX_COMMAND_LENGTH} chars)`;
93
+ }
94
+
95
+ if (blockedPattern.test(command)) {
96
+ return 'Shell operators are not allowed (|, &, ;, <, >, `, $, (, ))';
97
+ }
98
+
99
+ for (const pattern of dangerousPatterns) {
100
+ if (pattern.test(command)) {
101
+ return `Dangerous pattern detected: ${pattern.source}`;
102
+ }
76
103
  }
77
104
 
78
105
  if (tokens.length === 0) {
@@ -82,11 +109,11 @@ const validateCommand = (command, tokens) => {
82
109
  const [baseCommand, subcommand, thirdToken] = tokens;
83
110
 
84
111
  if (!allowedCommands.has(baseCommand)) {
85
- return `Command not allowed: ${baseCommand}`;
112
+ return `Command not allowed: ${baseCommand}. Allowed: ${[...allowedCommands].join(', ')}`;
86
113
  }
87
114
 
88
115
  if (baseCommand === 'git' && !allowedGitSubcommands.has(subcommand)) {
89
- return `Git subcommand not allowed: ${subcommand ?? '(missing)'}`;
116
+ return `Git subcommand not allowed: ${subcommand ?? '(missing)'}. Allowed: ${[...allowedGitSubcommands].join(', ')}`;
90
117
  }
91
118
 
92
119
  if (baseCommand === 'find') {
@@ -99,11 +126,11 @@ const validateCommand = (command, tokens) => {
99
126
 
100
127
  if (['npm', 'pnpm', 'yarn', 'bun'].includes(baseCommand)) {
101
128
  if (!subcommand || !allowedPackageManagerSubcommands.has(subcommand)) {
102
- return `Package manager subcommand not allowed: ${subcommand ?? '(missing)'}`;
129
+ return `Package manager subcommand not allowed: ${subcommand ?? '(missing)'}. Allowed: ${[...allowedPackageManagerSubcommands].join(', ')}`;
103
130
  }
104
131
 
105
132
  if (subcommand === 'run' && (!thirdToken || !safeRunScriptPattern.test(thirdToken))) {
106
- return `Package manager script not allowed: ${thirdToken ?? '(missing)'}`;
133
+ return `Package manager script not allowed: ${thirdToken ?? '(missing)'}. Allowed pattern: ${safeRunScriptPattern.source}`;
107
134
  }
108
135
  }
109
136
 
@@ -197,6 +224,32 @@ export const smartShell = async ({ command }) => {
197
224
  });
198
225
 
199
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
+ });
200
253
 
201
254
  const result = {
202
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
- ...buildSummaryMetrics(rawTokens, tokens),
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
- ...buildSummaryMetrics(rawTokens, tokens),
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,
@@ -205,6 +205,7 @@ const startTurn = async ({
205
205
  })
206
206
  : null;
207
207
 
208
+
208
209
  return {
209
210
  phase: 'start',
210
211
  promptPreview: truncate(prompt, MAX_PROMPT_PREVIEW),
@@ -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
+ };
@@ -0,0 +1,53 @@
1
+ // Stub for workflow tracking (to be implemented in future version)
2
+ // This avoids SQLite issues in tests while keeping the API available
3
+
4
+ export const detectWorkflowType = () => null;
5
+ export const getWorkflowBaseline = () => 0;
6
+ export const startWorkflow = () => null;
7
+ export const endWorkflow = () => null;
8
+ export const getWorkflowMetrics = () => [];
9
+ export const getWorkflowSummaryByType = () => [];
10
+ export const autoTrackWorkflow = () => null;
11
+
12
+ export const WORKFLOW_DEFINITIONS = {
13
+ debugging: {
14
+ name: 'Debugging',
15
+ description: 'Error-first, symbol-focused debugging workflow',
16
+ typicalTools: ['smart_turn', 'smart_search', 'smart_read', 'smart_shell'],
17
+ minTools: 3,
18
+ baselineTokens: 150000,
19
+ pattern: /debug|error|bug|fix|fail/i,
20
+ },
21
+ 'code-review': {
22
+ name: 'Code Review',
23
+ description: 'Diff-aware, API-focused code review workflow',
24
+ typicalTools: ['smart_turn', 'smart_context', 'smart_read', 'git_blame', 'smart_shell'],
25
+ minTools: 3,
26
+ baselineTokens: 200000,
27
+ pattern: /review|pr|pull.?request|approve/i,
28
+ },
29
+ refactoring: {
30
+ name: 'Refactoring',
31
+ description: 'Graph-aware, test-verified refactoring workflow',
32
+ typicalTools: ['smart_turn', 'smart_context', 'smart_read', 'git_blame', 'smart_shell'],
33
+ minTools: 3,
34
+ baselineTokens: 180000,
35
+ pattern: /refactor|extract|rename|move|restructure/i,
36
+ },
37
+ testing: {
38
+ name: 'Testing',
39
+ description: 'Coverage-aware, TDD-friendly testing workflow',
40
+ typicalTools: ['smart_turn', 'smart_search', 'smart_read', 'smart_context', 'smart_shell'],
41
+ minTools: 3,
42
+ baselineTokens: 120000,
43
+ pattern: /test|spec|coverage|tdd/i,
44
+ },
45
+ architecture: {
46
+ name: 'Architecture Exploration',
47
+ description: 'Index-first, minimal-detail architecture exploration',
48
+ typicalTools: ['smart_turn', 'smart_context', 'smart_search', 'smart_read', 'cross_project'],
49
+ minTools: 3,
50
+ baselineTokens: 300000,
51
+ pattern: /architect|explore|understand|structure|design/i,
52
+ },
53
+ };