smart-context-mcp 1.3.1 → 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,52 +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
- * ENABLED BY DEFAULT - disable with: DEVCTX_SHOW_USAGE=false
6
- *
7
- * Shows feedback after every devctx tool call to ensure visibility.
8
- * User can explicitly disable if they find it too verbose.
9
- */
10
-
11
1
  const sessionUsage = {
12
- tools: new Map(), // toolName -> { count, savedTokens }
2
+ tools: new Map(),
13
3
  totalSavedTokens: 0,
14
- enabled: true, // Changed: enabled by default
4
+ enabled: true,
15
5
  totalToolCalls: 0,
16
6
  };
17
7
 
18
- /**
19
- * Check if usage feedback is enabled
20
- *
21
- * Priority:
22
- * 1. Explicit env var (DEVCTX_SHOW_USAGE=true/false)
23
- * 2. Default: ENABLED (changed from disabled)
24
- */
25
8
  export const isFeedbackEnabled = () => {
26
9
  const envValue = process.env.DEVCTX_SHOW_USAGE?.toLowerCase();
27
10
 
28
- // Explicit enable
29
11
  if (envValue === 'true' || envValue === '1' || envValue === 'yes') {
30
12
  sessionUsage.enabled = true;
31
13
  return true;
32
14
  }
33
15
 
34
- // Explicit disable
35
16
  if (envValue === 'false' || envValue === '0' || envValue === 'no') {
36
17
  sessionUsage.enabled = false;
37
18
  return false;
38
19
  }
39
20
 
40
- // Default: ENABLED (changed)
41
21
  sessionUsage.enabled = true;
42
22
  return true;
43
23
  };
44
24
 
45
- /**
46
- * Record tool usage for feedback
47
- */
48
25
  export const recordToolUsage = ({ tool, savedTokens = 0, target = null }) => {
49
- // Increment total tool calls (for onboarding mode)
50
26
  sessionUsage.totalToolCalls += 1;
51
27
 
52
28
  if (!isFeedbackEnabled()) return;
@@ -60,24 +36,18 @@ export const recordToolUsage = ({ tool, savedTokens = 0, target = null }) => {
60
36
  sessionUsage.totalSavedTokens += savedTokens;
61
37
  };
62
38
 
63
- /**
64
- * Get current session usage stats
65
- */
66
39
  export const getSessionUsage = () => {
67
40
  return {
68
41
  tools: Array.from(sessionUsage.tools.entries()).map(([tool, stats]) => ({
69
42
  tool,
70
43
  count: stats.count,
71
44
  savedTokens: stats.savedTokens,
72
- targets: stats.targets.slice(-3), // Last 3 targets only
45
+ targets: stats.targets.slice(-3),
73
46
  })),
74
47
  totalSavedTokens: sessionUsage.totalSavedTokens,
75
48
  };
76
49
  };
77
50
 
78
- /**
79
- * Format usage feedback as markdown
80
- */
81
51
  export const formatUsageFeedback = () => {
82
52
  if (!isFeedbackEnabled()) return '';
83
53
 
@@ -118,19 +88,13 @@ export const formatUsageFeedback = () => {
118
88
  return lines.join('\n');
119
89
  };
120
90
 
121
- /**
122
- * Reset session usage (for testing or manual reset)
123
- */
124
91
  export const resetSessionUsage = () => {
125
92
  sessionUsage.tools.clear();
126
93
  sessionUsage.totalSavedTokens = 0;
127
94
  sessionUsage.totalToolCalls = 0;
128
- sessionUsage.enabled = true; // Reset to default (enabled)
95
+ sessionUsage.enabled = true;
129
96
  };
130
97
 
131
- /**
132
- * Format token count for display
133
- */
134
98
  const formatTokens = (tokens) => {
135
99
  if (tokens >= 1000000) {
136
100
  return `${(tokens / 1000000).toFixed(1)}M tokens`;
@@ -141,9 +105,6 @@ const formatTokens = (tokens) => {
141
105
  return `${tokens} tokens`;
142
106
  };
143
107
 
144
- /**
145
- * Truncate target path for display
146
- */
147
108
  const truncateTarget = (target) => {
148
109
  if (!target) return '';
149
110
  if (target.length <= 40) return target;