smart-context-mcp 1.7.1 → 1.7.2
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/package.json +1 -1
- package/src/metrics.js +13 -0
- package/src/orchestration/base-orchestrator.js +8 -2
- package/src/orchestration/policy/event-policy.js +37 -1
- package/src/task-runner.js +7 -0
- package/src/tools/smart-context.js +50 -1
- package/src/tools/smart-read.js +44 -1
- package/src/tools/smart-search.js +34 -1
- package/src/tools/smart-shell.js +9 -0
- package/src/utils/metrics-display.js +63 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-context-mcp",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
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",
|
package/src/metrics.js
CHANGED
|
@@ -267,6 +267,18 @@ export const aggregateMetrics = (entries) => {
|
|
|
267
267
|
|
|
268
268
|
const netSavedTokens = getNetSavedTokens(savedTokens, overheadTokens);
|
|
269
269
|
|
|
270
|
+
const topTools = tools
|
|
271
|
+
.slice()
|
|
272
|
+
.sort((a, b) => b.netSavedTokens - a.netSavedTokens)
|
|
273
|
+
.slice(0, 3)
|
|
274
|
+
.filter((tool) => tool.netSavedTokens > 0)
|
|
275
|
+
.map((tool) => ({
|
|
276
|
+
tool: tool.tool,
|
|
277
|
+
netSavedTokens: tool.netSavedTokens,
|
|
278
|
+
netSavingsPct: tool.netSavingsPct,
|
|
279
|
+
count: tool.count,
|
|
280
|
+
}));
|
|
281
|
+
|
|
270
282
|
return {
|
|
271
283
|
count: entries.length,
|
|
272
284
|
rawTokens,
|
|
@@ -275,6 +287,7 @@ export const aggregateMetrics = (entries) => {
|
|
|
275
287
|
savingsPct: rawTokens > 0 ? Number(((savedTokens / rawTokens) * 100).toFixed(2)) : 0,
|
|
276
288
|
netSavedTokens,
|
|
277
289
|
netSavingsPct: rawTokens > 0 ? Number(((netSavedTokens / rawTokens) * 100).toFixed(2)) : 0,
|
|
290
|
+
topTools,
|
|
278
291
|
tools,
|
|
279
292
|
overheadTokens,
|
|
280
293
|
overheadPctOfRaw: rawTokens > 0 ? Number(((overheadTokens / rawTokens) * 100).toFixed(2)) : 0,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
extractNextStep,
|
|
9
9
|
normalizeWhitespace,
|
|
10
10
|
truncate,
|
|
11
|
+
isSimpleTask,
|
|
11
12
|
MAX_FOCUS_LENGTH,
|
|
12
13
|
MAX_GOAL_LENGTH,
|
|
13
14
|
} from './policy/event-policy.js';
|
|
@@ -113,21 +114,25 @@ export const resolveManagedStart = async ({
|
|
|
113
114
|
startMaxTokens = DEFAULT_START_MAX_TOKENS,
|
|
114
115
|
startTurn = smartTurn,
|
|
115
116
|
summaryTool = smartSummary,
|
|
117
|
+
enableFastPath = true,
|
|
116
118
|
}) => {
|
|
119
|
+
const simpleTask = enableFastPath && isSimpleTask(prompt);
|
|
120
|
+
|
|
117
121
|
const startResult = preparedStartResult ?? await startTurn({
|
|
118
122
|
phase: 'start',
|
|
119
123
|
sessionId,
|
|
120
124
|
prompt,
|
|
121
|
-
ensureSession,
|
|
125
|
+
ensureSession: simpleTask ? false : ensureSession,
|
|
122
126
|
maxTokens: startMaxTokens,
|
|
123
127
|
});
|
|
124
128
|
|
|
125
|
-
if (!allowIsolation) {
|
|
129
|
+
if (!allowIsolation || simpleTask) {
|
|
126
130
|
return {
|
|
127
131
|
startResult,
|
|
128
132
|
isolated: Boolean(startResult?.isolatedSession),
|
|
129
133
|
previousSessionId: startResult?.previousSessionId ?? null,
|
|
130
134
|
autoStarted: !preparedStartResult,
|
|
135
|
+
fastPath: simpleTask,
|
|
131
136
|
};
|
|
132
137
|
}
|
|
133
138
|
|
|
@@ -143,6 +148,7 @@ export const resolveManagedStart = async ({
|
|
|
143
148
|
return {
|
|
144
149
|
...isolatedSession,
|
|
145
150
|
autoStarted: !preparedStartResult,
|
|
151
|
+
fastPath: false,
|
|
146
152
|
};
|
|
147
153
|
};
|
|
148
154
|
|
|
@@ -13,6 +13,39 @@ export const MIN_NEXT_STEP_LENGTH = 12;
|
|
|
13
13
|
export const MAX_NEXT_STEP_CAPTURE_LENGTH = 180;
|
|
14
14
|
export const MAX_RECOMMENDED_TOOLS = 3;
|
|
15
15
|
|
|
16
|
+
const SIMPLE_TASK_PATTERNS = [
|
|
17
|
+
/^(move|rename|delete|add|remove|fix|update|change|modify|edit)\s+\w+/i,
|
|
18
|
+
/^(create|write|read|show|display|list|find)\s+(a|an|the)?\s*\w+/i,
|
|
19
|
+
/\b(one|single|this|that)\s+(file|function|class|component|method)\b/i,
|
|
20
|
+
/^(add|remove|update|fix)\s+(comment|import|export|type|interface)/i,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const COMPLEX_TASK_INDICATORS = [
|
|
24
|
+
/\b(refactor|migrate|redesign|architect|implement|integrate)\b/i,
|
|
25
|
+
/\b(all|every|entire|whole|across|throughout)\b/i,
|
|
26
|
+
/\b(system|architecture|infrastructure|framework)\b/i,
|
|
27
|
+
/\b(multiple|several|many)\s+(files|components|modules)\b/i,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const isSimpleTask = (prompt) => {
|
|
31
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const normalized = normalizeWhitespace(prompt);
|
|
36
|
+
|
|
37
|
+
if (normalized.length > 200) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const hasComplexIndicator = COMPLEX_TASK_INDICATORS.some((pattern) => pattern.test(normalized));
|
|
42
|
+
if (hasComplexIndicator) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return SIMPLE_TASK_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
47
|
+
};
|
|
48
|
+
|
|
16
49
|
export const normalizeWhitespace = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
17
50
|
|
|
18
51
|
export const truncate = (value, maxLength = DEFAULT_TRUNCATE_LENGTH) => {
|
|
@@ -132,9 +165,10 @@ export const runWorkflowPreflight = async ({
|
|
|
132
165
|
startResult,
|
|
133
166
|
contextTool = smartContext,
|
|
134
167
|
searchTool = smartSearch,
|
|
168
|
+
skipPreflight = false,
|
|
135
169
|
}) => {
|
|
136
170
|
const preflight = workflowProfile.preflight;
|
|
137
|
-
if (!preflight) {
|
|
171
|
+
if (!preflight || skipPreflight) {
|
|
138
172
|
return null;
|
|
139
173
|
}
|
|
140
174
|
|
|
@@ -281,6 +315,7 @@ export const buildTaskRunnerAutomaticity = ({
|
|
|
281
315
|
usedWrapper = false,
|
|
282
316
|
overheadTokens = 0,
|
|
283
317
|
managedByBaseOrchestrator = false,
|
|
318
|
+
fastPath = false,
|
|
284
319
|
}) => {
|
|
285
320
|
const safeOverheadTokens = Number.isFinite(overheadTokens) ? Math.max(0, overheadTokens) : 0;
|
|
286
321
|
const checkpointPersisted = Boolean(endResult && !endResult.checkpoint?.skipped && !endResult.checkpoint?.blocked);
|
|
@@ -293,5 +328,6 @@ export const buildTaskRunnerAutomaticity = ({
|
|
|
293
328
|
autoWrappedPrompt: usedWrapper && safeOverheadTokens > 0,
|
|
294
329
|
isolatedSession: Boolean(startResult?.isolatedSession),
|
|
295
330
|
contextOverheadTokens: safeOverheadTokens,
|
|
331
|
+
fastPath,
|
|
296
332
|
};
|
|
297
333
|
};
|
package/src/task-runner.js
CHANGED
|
@@ -62,6 +62,7 @@ const recordRunnerMetrics = async ({
|
|
|
62
62
|
usedWrapper = false,
|
|
63
63
|
blocked = false,
|
|
64
64
|
doctorIssued = false,
|
|
65
|
+
fastPath = false,
|
|
65
66
|
}) => {
|
|
66
67
|
const startResult = result?.start ?? result?.startResult ?? null;
|
|
67
68
|
const endResult = result?.end ?? (result?.phase === 'end' ? result : null);
|
|
@@ -83,6 +84,7 @@ const recordRunnerMetrics = async ({
|
|
|
83
84
|
usedWrapper,
|
|
84
85
|
overheadTokens: Number(result?.overheadTokens ?? 0),
|
|
85
86
|
managedByBaseOrchestrator: WORKFLOW_COMMANDS.has(commandName),
|
|
87
|
+
fastPath,
|
|
86
88
|
});
|
|
87
89
|
|
|
88
90
|
await persistMetrics({
|
|
@@ -148,8 +150,10 @@ const runWorkflowCommand = async ({
|
|
|
148
150
|
ensureSession: true,
|
|
149
151
|
allowIsolation: false,
|
|
150
152
|
startMaxTokens: DEFAULT_START_MAX_TOKENS,
|
|
153
|
+
enableFastPath: true,
|
|
151
154
|
}));
|
|
152
155
|
const start = startResolution.startResult;
|
|
156
|
+
const fastPath = startResolution.fastPath ?? false;
|
|
153
157
|
|
|
154
158
|
const gate = evaluateRunnerGate({ startResult: start });
|
|
155
159
|
let preflightSummary = null;
|
|
@@ -159,6 +163,7 @@ const runWorkflowCommand = async ({
|
|
|
159
163
|
workflowProfile,
|
|
160
164
|
prompt: requestedPrompt,
|
|
161
165
|
startResult: start,
|
|
166
|
+
skipPreflight: fastPath,
|
|
162
167
|
});
|
|
163
168
|
preflightSummary = buildPreflightSummary(preflightResult);
|
|
164
169
|
}
|
|
@@ -194,6 +199,7 @@ const runWorkflowCommand = async ({
|
|
|
194
199
|
usedWrapper: false,
|
|
195
200
|
blocked: true,
|
|
196
201
|
doctorIssued: true,
|
|
202
|
+
fastPath,
|
|
197
203
|
});
|
|
198
204
|
return blockedResult;
|
|
199
205
|
}
|
|
@@ -231,6 +237,7 @@ const runWorkflowCommand = async ({
|
|
|
231
237
|
usedWrapper: true,
|
|
232
238
|
blocked: false,
|
|
233
239
|
doctorIssued: false,
|
|
240
|
+
fastPath,
|
|
234
241
|
});
|
|
235
242
|
return result;
|
|
236
243
|
};
|
|
@@ -14,6 +14,8 @@ import { predictContextFiles, recordContextAccess } from '../context-patterns.js
|
|
|
14
14
|
import { recordToolUsage } from '../usage-feedback.js';
|
|
15
15
|
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
16
16
|
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
17
|
+
import { buildMetricsDisplay } from '../utils/metrics-display.js';
|
|
18
|
+
import { createProgressReporter } from '../streaming.js';
|
|
17
19
|
import {
|
|
18
20
|
getDetailedDiff,
|
|
19
21
|
analyzeChangeImpact,
|
|
@@ -385,7 +387,15 @@ export const smartContext = async ({
|
|
|
385
387
|
detail = 'balanced',
|
|
386
388
|
include = DEFAULT_INCLUDE,
|
|
387
389
|
prefetch = false,
|
|
390
|
+
progress: enableProgress = false,
|
|
388
391
|
}) => {
|
|
392
|
+
const progress = enableProgress ? createProgressReporter('smart_context') : null;
|
|
393
|
+
const startTime = Date.now();
|
|
394
|
+
|
|
395
|
+
if (progress) {
|
|
396
|
+
progress.report({ phase: 'planning', task: task.substring(0, 80) });
|
|
397
|
+
}
|
|
398
|
+
|
|
389
399
|
const resolvedIntent = (intent && VALID_INTENTS.has(intent)) ? intent : inferIntent(task);
|
|
390
400
|
const root = projectRoot;
|
|
391
401
|
const detailMode = VALID_DETAIL_MODES.has(detail) ? detail : 'balanced';
|
|
@@ -438,6 +448,14 @@ export const smartContext = async ({
|
|
|
438
448
|
return impactB - impactA;
|
|
439
449
|
});
|
|
440
450
|
|
|
451
|
+
if (progress) {
|
|
452
|
+
progress.report({
|
|
453
|
+
phase: 'diff-analysis',
|
|
454
|
+
changedFiles: changed.files.length,
|
|
455
|
+
expandedFiles: expandedFiles.size,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
441
459
|
diffSummary = {
|
|
442
460
|
ref: changed.ref,
|
|
443
461
|
totalChanged: changed.files.length + changed.skippedDeleted,
|
|
@@ -467,6 +485,10 @@ export const smartContext = async ({
|
|
|
467
485
|
...fallbackKeywords,
|
|
468
486
|
extractFallbackSearchQuery(task),
|
|
469
487
|
]).slice(0, 6);
|
|
488
|
+
if (progress) {
|
|
489
|
+
progress.report({ phase: 'searching', queries: queryCandidates.length });
|
|
490
|
+
}
|
|
491
|
+
|
|
470
492
|
const searchResults = await Promise.all(
|
|
471
493
|
queryCandidates.map((query) => smartSearch({ query, cwd: '.', intent: resolvedIntent }))
|
|
472
494
|
);
|
|
@@ -602,6 +624,10 @@ export const smartContext = async ({
|
|
|
602
624
|
}
|
|
603
625
|
|
|
604
626
|
if (pendingReads.length > 0) {
|
|
627
|
+
if (progress) {
|
|
628
|
+
progress.report({ phase: 'reading', files: pendingReads.length });
|
|
629
|
+
}
|
|
630
|
+
|
|
605
631
|
const batchResults = await smartReadBatch({
|
|
606
632
|
files: pendingReads.map(({ item }) => ({ path: item.absPath, mode: item.mode })),
|
|
607
633
|
});
|
|
@@ -796,6 +822,28 @@ export const smartContext = async ({
|
|
|
796
822
|
tests: coverageMin(perFile.map((c) => c.tests)),
|
|
797
823
|
};
|
|
798
824
|
|
|
825
|
+
const filesIncluded = new Set(context.map((c) => c.file)).size;
|
|
826
|
+
const metricsDisplay = buildMetricsDisplay({
|
|
827
|
+
tool: 'smart_context',
|
|
828
|
+
target: task,
|
|
829
|
+
metrics: {
|
|
830
|
+
rawTokens: totalRawTokens,
|
|
831
|
+
compressedTokens: totalCompressedTokens,
|
|
832
|
+
savedTokens,
|
|
833
|
+
},
|
|
834
|
+
startTime: enableProgress ? startTime : null,
|
|
835
|
+
filesCount: filesIncluded,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
if (progress) {
|
|
839
|
+
progress.complete({
|
|
840
|
+
task: task.substring(0, 80),
|
|
841
|
+
files: filesIncluded,
|
|
842
|
+
savedTokens,
|
|
843
|
+
savingsPct: totalRawTokens > 0 ? ((savedTokens / totalRawTokens) * 100).toFixed(1) : null,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
799
847
|
const result = {
|
|
800
848
|
success: true,
|
|
801
849
|
task,
|
|
@@ -807,7 +855,7 @@ export const smartContext = async ({
|
|
|
807
855
|
metrics: {
|
|
808
856
|
contentTokens,
|
|
809
857
|
totalTokens: 0,
|
|
810
|
-
filesIncluded
|
|
858
|
+
filesIncluded,
|
|
811
859
|
filesEvaluated: expanded.size,
|
|
812
860
|
savingsPct,
|
|
813
861
|
detailMode,
|
|
@@ -825,6 +873,7 @@ export const smartContext = async ({
|
|
|
825
873
|
}
|
|
826
874
|
} : {})
|
|
827
875
|
},
|
|
876
|
+
metricsDisplay,
|
|
828
877
|
...(includeSet.has('hints') ? { hints } : {}),
|
|
829
878
|
};
|
|
830
879
|
|
package/src/tools/smart-read.js
CHANGED
|
@@ -12,6 +12,8 @@ import { countTokens } from '../tokenCounter.js';
|
|
|
12
12
|
import { recordToolUsage } from '../usage-feedback.js';
|
|
13
13
|
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
14
14
|
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
15
|
+
import { buildMetricsDisplay } from '../utils/metrics-display.js';
|
|
16
|
+
import { createProgressReporter } from '../streaming.js';
|
|
15
17
|
|
|
16
18
|
const execFile = promisify(execFileCb);
|
|
17
19
|
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';
|
|
@@ -408,14 +410,26 @@ const formatContextSections = (sections) => {
|
|
|
408
410
|
return parts.length > 0 ? '\n' + parts.join('\n') : '';
|
|
409
411
|
};
|
|
410
412
|
|
|
411
|
-
export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine, symbol, maxTokens, context: includeContext, cwd }) => {
|
|
413
|
+
export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine, symbol, maxTokens, context: includeContext, cwd, progress: enableProgress = false }) => {
|
|
414
|
+
const progress = enableProgress ? createProgressReporter('smart_read') : null;
|
|
415
|
+
const startTime = Date.now();
|
|
416
|
+
|
|
412
417
|
let fullPath, content;
|
|
413
418
|
const effectiveRoot = cwd || projectRoot;
|
|
414
419
|
|
|
420
|
+
if (progress) {
|
|
421
|
+
progress.report({ phase: 'reading', file: filePath });
|
|
422
|
+
}
|
|
423
|
+
|
|
415
424
|
try {
|
|
416
425
|
const result = readTextFile(filePath, effectiveRoot);
|
|
417
426
|
fullPath = result.fullPath;
|
|
418
427
|
content = result.content;
|
|
428
|
+
|
|
429
|
+
if (progress) {
|
|
430
|
+
const rawTokens = countTokens(content);
|
|
431
|
+
progress.report({ phase: 'loaded', file: path.relative(effectiveRoot, fullPath), rawTokens });
|
|
432
|
+
}
|
|
419
433
|
} catch (error) {
|
|
420
434
|
const errorMessage = error.message || String(error);
|
|
421
435
|
return {
|
|
@@ -479,6 +493,18 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
479
493
|
cacheHit = g.cached;
|
|
480
494
|
}
|
|
481
495
|
|
|
496
|
+
if (progress) {
|
|
497
|
+
const compressedTokens = countTokens(compressedText);
|
|
498
|
+
const rawTokens = countTokens(content);
|
|
499
|
+
progress.report({
|
|
500
|
+
phase: 'compressed',
|
|
501
|
+
mode: effectiveMode,
|
|
502
|
+
rawTokens,
|
|
503
|
+
compressedTokens,
|
|
504
|
+
ratio: rawTokens > 0 ? (rawTokens / compressedTokens).toFixed(1) : null,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
482
508
|
let contextResult = null;
|
|
483
509
|
|
|
484
510
|
if (mode === 'symbol' && includeContext && symbol) {
|
|
@@ -545,6 +571,22 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
545
571
|
const confidence = { parser, truncated, cached: cacheHit && !contextResult };
|
|
546
572
|
if (contextResult) confidence.graphCoverage = contextResult.graphCoverage;
|
|
547
573
|
|
|
574
|
+
const metricsDisplay = buildMetricsDisplay({
|
|
575
|
+
tool: 'smart_read',
|
|
576
|
+
target: path.relative(effectiveRoot, fullPath),
|
|
577
|
+
metrics,
|
|
578
|
+
startTime: enableProgress ? startTime : null,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
if (progress) {
|
|
582
|
+
progress.complete({
|
|
583
|
+
file: path.relative(effectiveRoot, fullPath),
|
|
584
|
+
mode: effectiveMode,
|
|
585
|
+
savedTokens: metrics.savedTokens,
|
|
586
|
+
savingsPct: metrics.savingsPct,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
548
590
|
const result = {
|
|
549
591
|
filePath: fullPath,
|
|
550
592
|
mode,
|
|
@@ -553,6 +595,7 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
553
595
|
content: compressedText,
|
|
554
596
|
confidence,
|
|
555
597
|
metrics,
|
|
598
|
+
metricsDisplay,
|
|
556
599
|
};
|
|
557
600
|
|
|
558
601
|
if (cacheHit && !contextResult) result.cached = true;
|
|
@@ -12,6 +12,8 @@ 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
14
|
import { IGNORED_DIRS, IGNORED_FILE_NAMES } from '../config/ignored-paths.js';
|
|
15
|
+
import { buildMetricsDisplay } from '../utils/metrics-display.js';
|
|
16
|
+
import { createProgressReporter } from '../streaming.js';
|
|
15
17
|
|
|
16
18
|
const execFile = promisify(execFileCallback);
|
|
17
19
|
const supportedGlobs = [
|
|
@@ -293,7 +295,14 @@ const buildCompactResult = (groups, totalMatches, query, root) => {
|
|
|
293
295
|
return lines.join('\n');
|
|
294
296
|
};
|
|
295
297
|
|
|
296
|
-
export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = false }) => {
|
|
298
|
+
export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = false, progress: enableProgress = false }) => {
|
|
299
|
+
const progress = enableProgress ? createProgressReporter('smart_search') : null;
|
|
300
|
+
const startTime = Date.now();
|
|
301
|
+
|
|
302
|
+
if (progress) {
|
|
303
|
+
progress.report({ phase: 'searching', query });
|
|
304
|
+
}
|
|
305
|
+
|
|
297
306
|
const root = resolveSafePath(cwd);
|
|
298
307
|
const rgMatches = _testForceWalk ? null : await searchWithRipgrep(root, query);
|
|
299
308
|
const usedFallback = rgMatches === null;
|
|
@@ -340,6 +349,11 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
340
349
|
let graphHits = null;
|
|
341
350
|
let indexFreshness = 'unavailable';
|
|
342
351
|
let loadedIndex = null;
|
|
352
|
+
|
|
353
|
+
if (progress) {
|
|
354
|
+
progress.report({ phase: 'ranking', rawMatches: rawMatches.length });
|
|
355
|
+
}
|
|
356
|
+
|
|
343
357
|
try {
|
|
344
358
|
loadedIndex = loadIndex(indexRoot);
|
|
345
359
|
if (loadedIndex) {
|
|
@@ -422,6 +436,24 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
422
436
|
|
|
423
437
|
const confidence = { level: retrievalConfidence, indexFreshness };
|
|
424
438
|
|
|
439
|
+
const metricsDisplay = buildMetricsDisplay({
|
|
440
|
+
tool: 'smart_search',
|
|
441
|
+
target: query,
|
|
442
|
+
metrics,
|
|
443
|
+
startTime: enableProgress ? startTime : null,
|
|
444
|
+
filesCount: groups.length,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (progress) {
|
|
448
|
+
progress.complete({
|
|
449
|
+
query,
|
|
450
|
+
matches: dedupedMatches.length,
|
|
451
|
+
files: groups.length,
|
|
452
|
+
savedTokens: metrics.savedTokens,
|
|
453
|
+
savingsPct: metrics.savingsPct,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
425
457
|
const result = {
|
|
426
458
|
query,
|
|
427
459
|
root,
|
|
@@ -437,6 +469,7 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
437
469
|
topFiles: groups.slice(0, 10).map((group) => ({ file: group.file, count: group.count, score: group.score })),
|
|
438
470
|
matches: compressedText,
|
|
439
471
|
metrics,
|
|
472
|
+
metricsDisplay,
|
|
440
473
|
};
|
|
441
474
|
|
|
442
475
|
if (provenance) result.provenance = provenance;
|
package/src/tools/smart-shell.js
CHANGED
|
@@ -7,6 +7,7 @@ import { pickRelevantLines, truncate, uniqueLines } from '../utils/text.js';
|
|
|
7
7
|
import { recordToolUsage } from '../usage-feedback.js';
|
|
8
8
|
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
9
9
|
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
10
|
+
import { buildMetricsDisplay } from '../utils/metrics-display.js';
|
|
10
11
|
|
|
11
12
|
const execFile = promisify(execFileCallback);
|
|
12
13
|
const isShellDisabled = () => process.env.DEVCTX_SHELL_DISABLED === 'true';
|
|
@@ -289,6 +290,13 @@ export const smartShell = async ({ command }) => {
|
|
|
289
290
|
context: `${outputLines} lines → ${compressedText.split('\n').length} lines (relevant only)`,
|
|
290
291
|
});
|
|
291
292
|
|
|
293
|
+
const metricsDisplay = buildMetricsDisplay({
|
|
294
|
+
tool: 'smart_shell',
|
|
295
|
+
target: command,
|
|
296
|
+
metrics,
|
|
297
|
+
startTime: null,
|
|
298
|
+
});
|
|
299
|
+
|
|
292
300
|
const result = {
|
|
293
301
|
command,
|
|
294
302
|
exitCode: execution.code,
|
|
@@ -296,6 +304,7 @@ export const smartShell = async ({ command }) => {
|
|
|
296
304
|
output: compressedText,
|
|
297
305
|
confidence: { blocked: false, timedOut: !!execution.timedOut },
|
|
298
306
|
metrics,
|
|
307
|
+
metricsDisplay,
|
|
299
308
|
};
|
|
300
309
|
|
|
301
310
|
if (execution.timedOut) result.timedOut = true;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const formatNumber = (value) => {
|
|
2
|
+
if (value >= 1000000) {
|
|
3
|
+
return `${(value / 1000000).toFixed(1)}M`;
|
|
4
|
+
}
|
|
5
|
+
if (value >= 1000) {
|
|
6
|
+
return `${(value / 1000).toFixed(1)}K`;
|
|
7
|
+
}
|
|
8
|
+
return String(value);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const formatRatio = (raw, compressed) => {
|
|
12
|
+
if (raw === 0 || compressed === 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const ratio = raw / compressed;
|
|
16
|
+
if (ratio < 2) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return `${ratio.toFixed(1)}:1`;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const formatDuration = (startTime) => {
|
|
23
|
+
if (!startTime) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const elapsed = Date.now() - startTime;
|
|
27
|
+
if (elapsed < 1000) {
|
|
28
|
+
return `${elapsed}ms`;
|
|
29
|
+
}
|
|
30
|
+
return `${(elapsed / 1000).toFixed(1)}s`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const buildMetricsDisplay = ({ tool, target, metrics, startTime, filesCount }) => {
|
|
34
|
+
const parts = [`✓ ${tool}`];
|
|
35
|
+
|
|
36
|
+
if (target) {
|
|
37
|
+
const shortTarget = target.length > 40 ? `${target.slice(0, 37)}...` : target;
|
|
38
|
+
parts.push(shortTarget);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (filesCount && filesCount > 1) {
|
|
42
|
+
parts.push(`${filesCount} files`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (metrics?.rawTokens > 0 && metrics?.compressedTokens > 0) {
|
|
46
|
+
const raw = formatNumber(metrics.rawTokens);
|
|
47
|
+
const compressed = formatNumber(metrics.compressedTokens);
|
|
48
|
+
const ratio = formatRatio(metrics.rawTokens, metrics.compressedTokens);
|
|
49
|
+
|
|
50
|
+
if (ratio) {
|
|
51
|
+
parts.push(`${raw}→${compressed} tokens (${ratio})`);
|
|
52
|
+
} else {
|
|
53
|
+
parts.push(`${raw}→${compressed} tokens`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const duration = formatDuration(startTime);
|
|
58
|
+
if (duration) {
|
|
59
|
+
parts.push(duration);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parts.join(', ');
|
|
63
|
+
};
|