smart-context-mcp 1.3.0 → 1.4.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.
@@ -0,0 +1,105 @@
1
+ import { readTextFile, writeTextFile } from '../utils/fs.js';
2
+ import { recordToolUsage } from '../usage-feedback.js';
3
+ import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
4
+ import { recordDevctxOperation } from '../missed-opportunities.js';
5
+ import { countTokens } from '../tokenCounter.js';
6
+ import { persistMetrics } from '../metrics.js';
7
+
8
+ export const smartEdit = ({ pattern, replacement, files, mode = 'literal', dryRun = false }) => {
9
+ const startTime = Date.now();
10
+
11
+ recordDecision({
12
+ tool: 'smart_edit',
13
+ reason: DECISION_REASONS.BATCH_OPERATION,
14
+ benefit: EXPECTED_BENEFITS.EFFICIENCY,
15
+ });
16
+
17
+ recordDevctxOperation('smart_edit');
18
+
19
+ const results = [];
20
+ let totalMatches = 0;
21
+ let totalReplacements = 0;
22
+
23
+ for (const filePath of files) {
24
+ try {
25
+ const { content } = readTextFile(filePath);
26
+ let newContent;
27
+ let matches = 0;
28
+
29
+ if (mode === 'regex') {
30
+ const regex = new RegExp(pattern, 'gm');
31
+ const matchesArray = content.match(regex);
32
+ matches = matchesArray ? matchesArray.length : 0;
33
+ newContent = content.replace(regex, replacement);
34
+ } else {
35
+ const parts = content.split(pattern);
36
+ matches = parts.length - 1;
37
+ newContent = parts.join(replacement);
38
+ }
39
+
40
+ if (matches > 0) {
41
+ if (!dryRun) {
42
+ writeTextFile(filePath, newContent);
43
+ }
44
+
45
+ results.push({
46
+ file: filePath,
47
+ matches,
48
+ replaced: !dryRun,
49
+ preview: dryRun ? {
50
+ before: content.substring(0, 200),
51
+ after: newContent.substring(0, 200),
52
+ } : undefined,
53
+ });
54
+
55
+ totalMatches += matches;
56
+ if (!dryRun) totalReplacements += matches;
57
+ } else {
58
+ results.push({
59
+ file: filePath,
60
+ matches: 0,
61
+ replaced: false,
62
+ });
63
+ }
64
+ } catch (error) {
65
+ results.push({
66
+ file: filePath,
67
+ error: error.message,
68
+ });
69
+ }
70
+ }
71
+
72
+ const response = {
73
+ success: true,
74
+ mode,
75
+ pattern,
76
+ dryRun,
77
+ totalFiles: files.length,
78
+ filesModified: results.filter(r => r.matches > 0).length,
79
+ totalMatches,
80
+ totalReplacements,
81
+ results,
82
+ };
83
+
84
+ const outputStr = JSON.stringify(response);
85
+ const tokens = countTokens(outputStr);
86
+
87
+ persistMetrics({
88
+ tool: 'smart_edit',
89
+ rawTokens: 0,
90
+ compressedTokens: tokens,
91
+ savedTokens: 0,
92
+ savingsPct: 0,
93
+ latencyMs: Date.now() - startTime,
94
+ });
95
+
96
+ recordToolUsage({
97
+ tool: 'smart_edit',
98
+ rawTokens: 0,
99
+ compressedTokens: tokens,
100
+ savedTokens: 0,
101
+ savingsPct: 0,
102
+ });
103
+
104
+ return response;
105
+ };
@@ -70,11 +70,11 @@ const extractRange = (content, startLine, endLine) => {
70
70
  return truncate(numbered.join('\n'), 12000);
71
71
  };
72
72
 
73
- const lookupIndexLine = (fullPath, symbolName) => {
73
+ const lookupIndexLine = (fullPath, symbolName, root = projectRoot) => {
74
74
  try {
75
- const index = loadIndex(projectRoot);
75
+ const index = loadIndex(root);
76
76
  if (!index) return { line: undefined, used: false };
77
- const relPath = path.relative(projectRoot, fullPath).replace(/\\/g, '/');
77
+ const relPath = path.relative(root, fullPath).replace(/\\/g, '/');
78
78
  const hits = queryIndex(index, symbolName);
79
79
  const match = hits.find((h) => h.path === relPath);
80
80
  return { line: match?.line, used: !!match };
@@ -255,6 +255,45 @@ export const grepSymbolInFile = async (absPath, symbol) => {
255
255
  }
256
256
  };
257
257
 
258
+ export const grepMultipleSymbolsInFile = async (absPath, symbols) => {
259
+ if (symbols.length === 0) return {};
260
+ if (symbols.length === 1) {
261
+ const matches = await grepSymbolInFile(absPath, symbols[0]);
262
+ return { [symbols[0]]: matches };
263
+ }
264
+
265
+ try {
266
+ const args = ['--line-number', '--no-heading', '--fixed-strings', '--max-count', '5'];
267
+ for (const sym of symbols) {
268
+ args.push('-e', sym);
269
+ }
270
+ args.push(absPath);
271
+
272
+ const { stdout } = await execFile(rgPath, args, { timeout: 3000 });
273
+ const result = {};
274
+ for (const sym of symbols) result[sym] = [];
275
+
276
+ for (const line of stdout.split('\n').filter(Boolean)) {
277
+ const sep = line.indexOf(':');
278
+ if (sep === -1) continue;
279
+ const formatted = `${line.substring(0, sep)}|${line.substring(sep + 1)}`;
280
+ const content = line.substring(sep + 1);
281
+
282
+ for (const sym of symbols) {
283
+ if (content.includes(sym)) {
284
+ result[sym].push(formatted);
285
+ }
286
+ }
287
+ }
288
+
289
+ return result;
290
+ } catch {
291
+ const result = {};
292
+ for (const sym of symbols) result[sym] = [];
293
+ return result;
294
+ }
295
+ };
296
+
258
297
  const TYPE_REF_RE = /:\s*([A-Z][A-Za-z0-9_]+)|<([A-Z][A-Za-z0-9_]+)>|(?:extends|implements)\s+([A-Z][A-Za-z0-9_]+)/g;
259
298
 
260
299
  export const extractTypeReferences = (definitionText, index, relPath) => {
@@ -292,8 +331,10 @@ export const buildSymbolContext = async (fullPath, symbolNames, root) => {
292
331
  for (const caller of related.importedBy.slice(0, 5)) {
293
332
  const callerAbs = path.join(root, caller);
294
333
  if (!fs.existsSync(callerAbs)) continue;
334
+
335
+ const matchesBySymbol = await grepMultipleSymbolsInFile(callerAbs, symbolNames);
295
336
  for (const sym of symbolNames) {
296
- const matches = await grepSymbolInFile(callerAbs, sym);
337
+ const matches = matchesBySymbol[sym] || [];
297
338
  if (matches.length > 0) {
298
339
  sections.callers.push({ file: caller, symbol: sym, lines: matches.slice(0, 3) });
299
340
  }
@@ -303,8 +344,10 @@ export const buildSymbolContext = async (fullPath, symbolNames, root) => {
303
344
  for (const testFile of related.tests.slice(0, 3)) {
304
345
  const testAbs = path.join(root, testFile);
305
346
  if (!fs.existsSync(testAbs)) continue;
347
+
348
+ const matchesBySymbol = await grepMultipleSymbolsInFile(testAbs, symbolNames);
306
349
  for (const sym of symbolNames) {
307
- const matches = await grepSymbolInFile(testAbs, sym);
350
+ const matches = matchesBySymbol[sym] || [];
308
351
  if (matches.length > 0) {
309
352
  sections.tests.push({ file: testFile, symbol: sym, lines: matches.slice(0, 3) });
310
353
  }
@@ -365,11 +408,12 @@ const formatContextSections = (sections) => {
365
408
  return parts.length > 0 ? '\n' + parts.join('\n') : '';
366
409
  };
367
410
 
368
- export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine, symbol, maxTokens, context: includeContext }) => {
411
+ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine, symbol, maxTokens, context: includeContext, cwd }) => {
369
412
  let fullPath, content;
413
+ const effectiveRoot = cwd || projectRoot;
370
414
 
371
415
  try {
372
- const result = readTextFile(filePath);
416
+ const result = readTextFile(filePath, effectiveRoot);
373
417
  fullPath = result.fullPath;
374
418
  content = result.content;
375
419
  } catch (error) {
@@ -400,6 +444,15 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
400
444
  const r = cachedRange(content, startLine, endLine, fullPath, mtime);
401
445
  compressedText = r.text;
402
446
  cacheHit = r.cached;
447
+ } else if (mode === 'outline' && (startLine || endLine)) {
448
+ const lines = content.split('\n');
449
+ const start = Math.max(0, (startLine ?? 1) - 1);
450
+ const end = endLine ?? lines.length;
451
+ const rangeContent = lines.slice(start, end).join('\n');
452
+ const g = cachedGenerate(fullPath, extension, rangeContent, 'outline', mtime);
453
+ compressedText = g.text;
454
+ cacheHit = g.cached;
455
+ effectiveMode = 'outline';
403
456
  } else if (mode === 'symbol') {
404
457
  const sym = cachedSymbol(fullPath, content, symbol, mtime);
405
458
  compressedText = sym.text;
@@ -430,7 +483,7 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
430
483
 
431
484
  if (mode === 'symbol' && includeContext && symbol) {
432
485
  const symbolNames = Array.isArray(symbol) ? symbol : [symbol];
433
- const { sections, hints } = await buildSymbolContext(fullPath, symbolNames, projectRoot);
486
+ const { sections, hints } = await buildSymbolContext(fullPath, symbolNames, effectiveRoot);
434
487
  const contextText = formatContextSections(sections);
435
488
  if (contextText) compressedText += contextText;
436
489
  contextResult = {
@@ -461,7 +514,7 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
461
514
  recordToolUsage({
462
515
  tool: 'smart_read',
463
516
  savedTokens: metrics.savedTokens,
464
- target: path.relative(projectRoot, fullPath),
517
+ target: path.relative(effectiveRoot, fullPath),
465
518
  });
466
519
 
467
520
  // Record devctx operation for missed opportunity detection
@@ -482,7 +535,7 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
482
535
 
483
536
  recordDecision({
484
537
  tool: 'smart_read',
485
- action: `read ${path.relative(projectRoot, fullPath)} (${effectiveMode} mode)`,
538
+ action: `read ${path.relative(effectiveRoot, fullPath)} (${effectiveMode} mode)`,
486
539
  reason,
487
540
  alternative: 'Read (full file)',
488
541
  expectedBenefit,
@@ -11,6 +11,7 @@ import { truncate } from '../utils/text.js';
11
11
  import { recordToolUsage } from '../usage-feedback.js';
12
12
  import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
13
13
  import { recordDevctxOperation } from '../missed-opportunities.js';
14
+ import { IGNORED_DIRS, IGNORED_FILE_NAMES } from '../config/ignored-paths.js';
14
15
 
15
16
  const execFile = promisify(execFileCallback);
16
17
  const supportedGlobs = [
@@ -19,8 +20,8 @@ const supportedGlobs = [
19
20
  '*.go', '*.rs', '*.java', '*.sh', '*.bash', '*.zsh', '*.tf', '*.tfvars', '*.hcl',
20
21
  'Dockerfile', 'Dockerfile.*',
21
22
  ];
22
- const ignoredDirs = ['node_modules', '.git', '.next', 'dist', 'build', 'coverage', '.venv', 'venv', '__pycache__', '.terraform'];
23
- const ignoredFileNames = new Set(['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'bun.lockb', 'npm-shrinkwrap.json']);
23
+ const ignoredDirs = IGNORED_DIRS;
24
+ const ignoredFileNames = new Set(IGNORED_FILE_NAMES);
24
25
  const fallbackExtensions = new Set(['.js', '.jsx', '.ts', '.tsx', '.json', '.mjs', '.cjs', '.py', '.toml', '.yaml', '.yml', '.md', '.graphql', '.gql', '.sql', '.go', '.rs', '.java', '.sh', '.bash', '.zsh', '.tf', '.tfvars', '.hcl']);
25
26
  const likelySourceExtensions = new Set(['.js', '.jsx', '.ts', '.tsx', '.py', '.graphql', '.gql', '.sql', '.go', '.rs', '.java', '.sh', '.bash', '.zsh']);
26
27
  const likelyConfigExtensions = new Set(['.json', '.toml', '.yaml', '.yml', '.tf', '.tfvars', '.hcl']);
@@ -127,6 +128,7 @@ const searchWithRipgrep = async (root, query) => {
127
128
  .filter((match) => !shouldIgnoreFile(match.file));
128
129
  } catch (error) {
129
130
  if (error.code === 1) return [];
131
+ console.error('[smart-search] ripgrep failed:', error.message, { code: error.code, signal: error.signal });
130
132
  return null;
131
133
  }
132
134
  };
@@ -0,0 +1,201 @@
1
+ import { withStateDb } from '../storage/sqlite.js';
2
+ import { recordToolUsage } from '../usage-feedback.js';
3
+ import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
4
+ import { recordDevctxOperation } from '../missed-opportunities.js';
5
+ import { countTokens } from '../tokenCounter.js';
6
+
7
+ const ACTIVE_SESSION_SCOPE = 'active';
8
+
9
+ const getActiveSession = async () => {
10
+ return withStateDb((db) => {
11
+ let activeSessionId = db.prepare(`
12
+ SELECT session_id
13
+ FROM active_session
14
+ WHERE scope = ?
15
+ `).get(ACTIVE_SESSION_SCOPE)?.session_id;
16
+
17
+ if (!activeSessionId) {
18
+ const mostRecent = db.prepare(`
19
+ SELECT session_id
20
+ FROM sessions
21
+ ORDER BY updated_at DESC
22
+ LIMIT 1
23
+ `).get();
24
+
25
+ if (!mostRecent) return null;
26
+ activeSessionId = mostRecent.session_id;
27
+ }
28
+
29
+ const row = db.prepare(`
30
+ SELECT session_id, goal, status, next_step, current_focus, why_blocked,
31
+ snapshot_json, completed_count, decisions_count, touched_files_count, updated_at
32
+ FROM sessions
33
+ WHERE session_id = ?
34
+ `).get(activeSessionId);
35
+
36
+ if (!row) return null;
37
+
38
+ const parseJsonField = (field) => {
39
+ if (!field) return [];
40
+ try {
41
+ return JSON.parse(field);
42
+ } catch {
43
+ return [];
44
+ }
45
+ };
46
+
47
+ const snapshot = parseJsonField(row.snapshot_json);
48
+
49
+ return {
50
+ sessionId: row.session_id,
51
+ goal: row.goal || 'Untitled session',
52
+ status: row.status || 'in_progress',
53
+ nextStep: row.next_step,
54
+ currentFocus: row.current_focus,
55
+ whyBlocked: row.why_blocked,
56
+ pinnedContext: snapshot.pinnedContext || [],
57
+ unresolvedQuestions: snapshot.unresolvedQuestions || [],
58
+ completed: snapshot.completed || [],
59
+ decisions: snapshot.decisions || [],
60
+ touchedFiles: snapshot.touchedFiles || [],
61
+ completedCount: row.completed_count || 0,
62
+ decisionsCount: row.decisions_count || 0,
63
+ touchedFilesCount: row.touched_files_count || 0,
64
+ updatedAt: row.updated_at,
65
+ };
66
+ });
67
+ };
68
+
69
+ const formatContextItem = (item, index, total) => {
70
+ const prefix = index === total - 1 ? '└─' : '├─';
71
+ return `${prefix} ${item}`;
72
+ };
73
+
74
+ const formatSection = (title, items, emptyMessage = 'None') => {
75
+ if (!items || items.length === 0) {
76
+ return `${title}:\n ${emptyMessage}`;
77
+ }
78
+
79
+ const formatted = items.map((item, i) => formatContextItem(item, i, items.length)).join('\n ');
80
+ return `${title} (${items.length}):\n ${formatted}`;
81
+ };
82
+
83
+ const compactPath = (filePath) => {
84
+ if (!filePath) return filePath;
85
+ const parts = filePath.split('/');
86
+ if (parts.length <= 3) return filePath;
87
+ return `.../${parts.slice(-3).join('/')}`;
88
+ };
89
+
90
+ export const smartStatus = async ({ format = 'detailed', maxItems = 10 } = {}) => {
91
+ const startTime = Date.now();
92
+
93
+ recordDecision({
94
+ tool: 'smart_status',
95
+ reason: DECISION_REASONS.CONTEXT_VISIBILITY,
96
+ benefit: EXPECTED_BENEFITS.TRANSPARENCY,
97
+ });
98
+
99
+ recordDevctxOperation('smart_status');
100
+
101
+ const session = await getActiveSession();
102
+
103
+ if (!session) {
104
+ const response = {
105
+ success: false,
106
+ message: 'No active session found',
107
+ hint: 'Use smart_summary with action=update to create a session',
108
+ };
109
+
110
+ recordToolUsage({
111
+ tool: 'smart_status',
112
+ rawTokens: 0,
113
+ compressedTokens: countTokens(JSON.stringify(response)),
114
+ savedTokens: 0,
115
+ savingsPct: 0,
116
+ });
117
+
118
+ return response;
119
+ }
120
+
121
+ const recentCompleted = session.completed.slice(-maxItems);
122
+ const recentDecisions = session.decisions.slice(-maxItems);
123
+ const recentFiles = session.touchedFiles.slice(-maxItems).map(compactPath);
124
+
125
+ let output;
126
+
127
+ if (format === 'compact') {
128
+ output = {
129
+ sessionId: session.sessionId,
130
+ status: session.status,
131
+ nextStep: session.nextStep,
132
+ stats: {
133
+ completed: session.completedCount,
134
+ decisions: session.decisionsCount,
135
+ files: session.touchedFilesCount,
136
+ },
137
+ recentFiles: recentFiles.slice(-3),
138
+ updatedAt: session.updatedAt,
139
+ };
140
+ } else {
141
+ const sections = [
142
+ `📋 Session: ${session.sessionId}`,
143
+ `🎯 Goal: ${session.goal}`,
144
+ `📊 Status: ${session.status}`,
145
+ session.nextStep ? `⏭️ Next: ${session.nextStep}` : null,
146
+ session.currentFocus ? `🔍 Focus: ${session.currentFocus}` : null,
147
+ session.whyBlocked ? `🚫 Blocked: ${session.whyBlocked}` : null,
148
+ '',
149
+ formatSection('✅ Completed', recentCompleted, 'No tasks completed yet'),
150
+ '',
151
+ formatSection('💡 Key Decisions', recentDecisions, 'No decisions recorded yet'),
152
+ '',
153
+ formatSection('📁 Touched Files', recentFiles, 'No files modified yet'),
154
+ '',
155
+ session.pinnedContext.length > 0 ? formatSection('📌 Pinned Context', session.pinnedContext) : null,
156
+ session.unresolvedQuestions.length > 0 ? formatSection('❓ Unresolved Questions', session.unresolvedQuestions) : null,
157
+ '',
158
+ `📈 Totals: ${session.completedCount} completed, ${session.decisionsCount} decisions, ${session.touchedFilesCount} files`,
159
+ `🕐 Updated: ${new Date(session.updatedAt).toLocaleString()}`,
160
+ ].filter(Boolean).join('\n');
161
+
162
+ output = {
163
+ success: true,
164
+ sessionId: session.sessionId,
165
+ status: session.status,
166
+ summary: sections,
167
+ context: {
168
+ goal: session.goal,
169
+ nextStep: session.nextStep,
170
+ currentFocus: session.currentFocus,
171
+ whyBlocked: session.whyBlocked,
172
+ stats: {
173
+ completed: session.completedCount,
174
+ decisions: session.decisionsCount,
175
+ files: session.touchedFilesCount,
176
+ },
177
+ recent: {
178
+ completed: recentCompleted,
179
+ decisions: recentDecisions,
180
+ files: recentFiles,
181
+ },
182
+ pinned: session.pinnedContext,
183
+ questions: session.unresolvedQuestions,
184
+ },
185
+ updatedAt: session.updatedAt,
186
+ };
187
+ }
188
+
189
+ const outputStr = JSON.stringify(output);
190
+ const tokens = countTokens(outputStr);
191
+
192
+ recordToolUsage({
193
+ tool: 'smart_status',
194
+ rawTokens: 0,
195
+ compressedTokens: tokens,
196
+ savedTokens: 0,
197
+ savingsPct: 0,
198
+ });
199
+
200
+ return output;
201
+ };
@@ -1106,8 +1106,37 @@ export const smartSummary = async ({
1106
1106
  keepLatestMetrics,
1107
1107
  vacuum,
1108
1108
  apply,
1109
+ goal,
1110
+ status,
1111
+ nextStep,
1112
+ currentFocus,
1113
+ whyBlocked,
1114
+ pinnedContext,
1115
+ unresolvedQuestions,
1116
+ blockers,
1117
+ completed,
1118
+ decisions,
1119
+ touchedFiles,
1109
1120
  } = {}) => {
1110
1121
  const startTime = Date.now();
1122
+
1123
+ if (!update && (goal || status || nextStep || currentFocus || whyBlocked ||
1124
+ pinnedContext || unresolvedQuestions || blockers || completed || decisions || touchedFiles)) {
1125
+ update = {
1126
+ goal,
1127
+ status,
1128
+ nextStep,
1129
+ currentFocus,
1130
+ whyBlocked,
1131
+ pinnedContext,
1132
+ unresolvedQuestions,
1133
+ blockers,
1134
+ completed,
1135
+ decisions,
1136
+ touchedFiles,
1137
+ };
1138
+ }
1139
+
1111
1140
  const mutationSafety = getMutationSafetyPolicy();
1112
1141
  const shouldBlockWrites = SUMMARY_WRITE_ACTIONS.has(action) && mutationSafety.shouldBlock;
1113
1142
  const allowReadSideEffects = !mutationSafety.shouldBlock;
@@ -1,67 +1,28 @@
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
1
  const sessionUsage = {
12
- tools: new Map(), // toolName -> { count, savedTokens }
2
+ tools: new Map(),
13
3
  totalSavedTokens: 0,
14
- enabled: false,
4
+ enabled: true,
15
5
  totalToolCalls: 0,
16
- onboardingMode: true,
17
- ONBOARDING_THRESHOLD: 10, // Auto-disable after 10 tool calls
18
6
  };
19
7
 
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
8
  export const isFeedbackEnabled = () => {
29
9
  const envValue = process.env.DEVCTX_SHOW_USAGE?.toLowerCase();
30
10
 
31
- // Explicit enable
32
11
  if (envValue === 'true' || envValue === '1' || envValue === 'yes') {
33
12
  sessionUsage.enabled = true;
34
- sessionUsage.onboardingMode = false;
35
13
  return true;
36
14
  }
37
15
 
38
- // Explicit disable
39
16
  if (envValue === 'false' || envValue === '0' || envValue === 'no') {
40
17
  sessionUsage.enabled = false;
41
- sessionUsage.onboardingMode = false;
42
18
  return false;
43
19
  }
44
20
 
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;
21
+ sessionUsage.enabled = true;
22
+ return true;
58
23
  };
59
24
 
60
- /**
61
- * Record tool usage for feedback
62
- */
63
25
  export const recordToolUsage = ({ tool, savedTokens = 0, target = null }) => {
64
- // Increment total tool calls (for onboarding mode)
65
26
  sessionUsage.totalToolCalls += 1;
66
27
 
67
28
  if (!isFeedbackEnabled()) return;
@@ -75,24 +36,18 @@ export const recordToolUsage = ({ tool, savedTokens = 0, target = null }) => {
75
36
  sessionUsage.totalSavedTokens += savedTokens;
76
37
  };
77
38
 
78
- /**
79
- * Get current session usage stats
80
- */
81
39
  export const getSessionUsage = () => {
82
40
  return {
83
41
  tools: Array.from(sessionUsage.tools.entries()).map(([tool, stats]) => ({
84
42
  tool,
85
43
  count: stats.count,
86
44
  savedTokens: stats.savedTokens,
87
- targets: stats.targets.slice(-3), // Last 3 targets only
45
+ targets: stats.targets.slice(-3),
88
46
  })),
89
47
  totalSavedTokens: sessionUsage.totalSavedTokens,
90
48
  };
91
49
  };
92
50
 
93
- /**
94
- * Format usage feedback as markdown
95
- */
96
51
  export const formatUsageFeedback = () => {
97
52
  if (!isFeedbackEnabled()) return '';
98
53
 
@@ -128,31 +83,18 @@ export const formatUsageFeedback = () => {
128
83
  }
129
84
 
130
85
  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
- }
86
+ lines.push('*To disable this message: `export DEVCTX_SHOW_USAGE=false`*');
139
87
 
140
88
  return lines.join('\n');
141
89
  };
142
90
 
143
- /**
144
- * Reset session usage (for testing or manual reset)
145
- */
146
91
  export const resetSessionUsage = () => {
147
92
  sessionUsage.tools.clear();
148
93
  sessionUsage.totalSavedTokens = 0;
149
94
  sessionUsage.totalToolCalls = 0;
150
- sessionUsage.onboardingMode = true;
95
+ sessionUsage.enabled = true;
151
96
  };
152
97
 
153
- /**
154
- * Format token count for display
155
- */
156
98
  const formatTokens = (tokens) => {
157
99
  if (tokens >= 1000000) {
158
100
  return `${(tokens / 1000000).toFixed(1)}M tokens`;
@@ -163,9 +105,6 @@ const formatTokens = (tokens) => {
163
105
  return `${tokens} tokens`;
164
106
  };
165
107
 
166
- /**
167
- * Truncate target path for display
168
- */
169
108
  const truncateTarget = (target) => {
170
109
  if (!target) return '';
171
110
  if (target.length <= 40) return target;