smart-context-mcp 1.9.0 → 1.11.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 +1 -1
- package/package.json +1 -1
- package/scripts/init-clients.js +1 -3
- package/scripts/report-adoption-metrics.js +0 -9
- package/scripts/report-workflow-metrics.js +0 -1
- package/server.json +2 -2
- package/src/index.js +1 -3
- package/src/missed-opportunities.js +0 -1
- package/src/server.js +4 -6
- package/src/tools/smart-context.js +2 -14
- package/src/tools/smart-read.js +0 -5
- package/src/tools/smart-search.js +86 -26
- package/src/tools/smart-shell.js +47 -6
- package/src/usage-feedback.js +0 -1
- package/src/workflow-tracker.js +0 -10
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Restart your AI client. Done.
|
|
|
56
56
|
# Check installed version
|
|
57
57
|
npm list -g smart-context-mcp
|
|
58
58
|
|
|
59
|
-
# Should show: smart-context-mcp@1.
|
|
59
|
+
# Should show: smart-context-mcp@1.11.0 (or later)
|
|
60
60
|
|
|
61
61
|
# Update to latest version
|
|
62
62
|
npm update -g smart-context-mcp
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-context-mcp",
|
|
3
3
|
"mcpName": "io.github.Arrayo/smart-context-mcp",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.11.0",
|
|
5
5
|
"description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
|
|
6
6
|
"author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
|
|
7
7
|
"type": "module",
|
package/scripts/init-clients.js
CHANGED
|
@@ -461,11 +461,9 @@ const updateCursorRule = (targetDir, dryRun) => {
|
|
|
461
461
|
const rulesDir = path.join(targetDir, '.cursor', 'rules');
|
|
462
462
|
const profilesDir = path.join(rulesDir, 'profiles-compact');
|
|
463
463
|
|
|
464
|
-
// Write base rule (always active)
|
|
465
464
|
const baseFilePath = path.join(rulesDir, 'devctx.mdc');
|
|
466
465
|
writeFile(baseFilePath, cursorRuleContent, dryRun);
|
|
467
|
-
|
|
468
|
-
// Write profiles README
|
|
466
|
+
|
|
469
467
|
const profilesReadmePath = path.join(profilesDir, 'README.md');
|
|
470
468
|
writeFile(profilesReadmePath, cursorProfilesNote, dryRun);
|
|
471
469
|
|
|
@@ -73,7 +73,6 @@ const calculateAdoptionMetrics = (days = 30) => {
|
|
|
73
73
|
return withStateDb((db) => {
|
|
74
74
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
75
75
|
|
|
76
|
-
// Get all sessions since cutoff
|
|
77
76
|
const sessions = db
|
|
78
77
|
.prepare(
|
|
79
78
|
`
|
|
@@ -94,7 +93,6 @@ const calculateAdoptionMetrics = (days = 30) => {
|
|
|
94
93
|
toolUsage: {},
|
|
95
94
|
};
|
|
96
95
|
|
|
97
|
-
// Initialize workflow stats
|
|
98
96
|
Object.keys(WORKFLOW_DEFINITIONS).forEach((type) => {
|
|
99
97
|
results.byWorkflow[type] = {
|
|
100
98
|
total: 0,
|
|
@@ -103,12 +101,10 @@ const calculateAdoptionMetrics = (days = 30) => {
|
|
|
103
101
|
};
|
|
104
102
|
});
|
|
105
103
|
|
|
106
|
-
// Analyze each session
|
|
107
104
|
sessions.forEach((session) => {
|
|
108
105
|
const snapshot = JSON.parse(session.snapshot_json || '{}');
|
|
109
106
|
const sessionId = session.session_id;
|
|
110
107
|
|
|
111
|
-
// Get events for this session
|
|
112
108
|
const sessionEvents = db
|
|
113
109
|
.prepare('SELECT * FROM session_events WHERE session_id = ?')
|
|
114
110
|
.all(sessionId);
|
|
@@ -117,25 +113,21 @@ const calculateAdoptionMetrics = (days = 30) => {
|
|
|
117
113
|
.prepare('SELECT * FROM metrics_events WHERE session_id = ?')
|
|
118
114
|
.all(sessionId);
|
|
119
115
|
|
|
120
|
-
// Check if non-trivial
|
|
121
116
|
if (!isNonTrivialTask(sessionEvents, metricsEvents)) {
|
|
122
117
|
return;
|
|
123
118
|
}
|
|
124
119
|
|
|
125
120
|
results.nonTrivialTasks++;
|
|
126
121
|
|
|
127
|
-
// Check if used devctx
|
|
128
122
|
const hasDevctx = usedDevctx(metricsEvents);
|
|
129
123
|
if (hasDevctx) {
|
|
130
124
|
results.tasksWithDevctx++;
|
|
131
125
|
}
|
|
132
126
|
|
|
133
|
-
// Track tool usage
|
|
134
127
|
metricsEvents.forEach((m) => {
|
|
135
128
|
results.toolUsage[m.tool] = (results.toolUsage[m.tool] || 0) + 1;
|
|
136
129
|
});
|
|
137
130
|
|
|
138
|
-
// Classify by workflow if possible
|
|
139
131
|
const goal = snapshot.goal || '';
|
|
140
132
|
let workflowType = null;
|
|
141
133
|
|
|
@@ -154,7 +146,6 @@ const calculateAdoptionMetrics = (days = 30) => {
|
|
|
154
146
|
}
|
|
155
147
|
});
|
|
156
148
|
|
|
157
|
-
// Calculate rates
|
|
158
149
|
if (results.nonTrivialTasks > 0) {
|
|
159
150
|
results.adoptionRate = (results.tasksWithDevctx / results.nonTrivialTasks) * 100;
|
|
160
151
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/Arrayo/smart-context-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.11.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "smart-context-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.11.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/src/index.js
CHANGED
|
@@ -726,9 +726,7 @@ export const buildIndex = (root, progress = null) => {
|
|
|
726
726
|
if (sym.snippet) entry.snippet = sym.snippet;
|
|
727
727
|
invertedIndex[key].push(entry);
|
|
728
728
|
}
|
|
729
|
-
} catch {
|
|
730
|
-
// skip unreadable files
|
|
731
|
-
}
|
|
729
|
+
} catch { /* unreadable */ }
|
|
732
730
|
|
|
733
731
|
processed++;
|
|
734
732
|
|
|
@@ -141,7 +141,6 @@ export const formatMissedOpportunities = () => {
|
|
|
141
141
|
lines.push('⚠️ **Missed devctx opportunities detected:**');
|
|
142
142
|
lines.push('');
|
|
143
143
|
|
|
144
|
-
// Show session stats
|
|
145
144
|
lines.push(`**Session stats:**`);
|
|
146
145
|
lines.push(`- Duration: ${analysis.sessionDuration}s`);
|
|
147
146
|
lines.push(`- devctx operations: ${analysis.devctxOperations}`);
|
package/src/server.js
CHANGED
|
@@ -57,10 +57,8 @@ export const createDevctxServer = () => {
|
|
|
57
57
|
version,
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
// Enable streaming progress notifications
|
|
61
60
|
setServerForStreaming(server);
|
|
62
61
|
|
|
63
|
-
// Register prompts
|
|
64
62
|
server.prompt(
|
|
65
63
|
'use-devctx',
|
|
66
64
|
'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.',
|
|
@@ -128,7 +126,7 @@ This ensures optimal performance and context recovery.`,
|
|
|
128
126
|
|
|
129
127
|
server.tool(
|
|
130
128
|
'smart_read',
|
|
131
|
-
'Read a file with token-efficient modes. outline/signatures
|
|
129
|
+
'Read a file with token-efficient modes. PREFER outline/signatures/symbol — full mode saves 0 tokens (same content as native Read, capped at 12k chars). Mode guide: outline (~90% savings): file structure, exports, top-level symbols — use for orientation, code review, deciding what to read next. signatures (~85% savings): function/method signatures only — use when you need parameter names and return types without bodies. symbol: extract one or more functions/classes/methods by name (string or array for batch) — use when you know exactly what to read; add context=true to include callers, tests, and referenced types from the dependency graph (returns graphCoverage: full|partial|none). range: specific line range with line numbers — use only when you need exact lines. full: raw file content, no compression, no savings — only use when the exact byte-for-byte content is required (e.g. config files, lock files). maxTokens: token budget — auto-selects the most detailed mode that fits (full → outline → signatures → truncated). 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.',
|
|
132
130
|
{
|
|
133
131
|
filePath: z.string(),
|
|
134
132
|
mode: z.enum(['full', 'outline', 'signatures', 'range', 'symbol']).optional(),
|
|
@@ -144,7 +142,7 @@ This ensures optimal performance and context recovery.`,
|
|
|
144
142
|
|
|
145
143
|
server.tool(
|
|
146
144
|
'smart_read_batch',
|
|
147
|
-
'Read multiple files in one call. Each item accepts path, mode, symbol, startLine, endLine, maxTokens (per-file budget). Optional global maxTokens budget with early stop when exceeded. Max 20 files per call.',
|
|
145
|
+
'Read multiple files in one call. Each item accepts path, mode (prefer outline/signatures/symbol — full saves 0 tokens), symbol, startLine, endLine, maxTokens (per-file budget). Optional global maxTokens budget with early stop when exceeded. Max 20 files per call.',
|
|
148
146
|
{
|
|
149
147
|
files: z.array(z.object({
|
|
150
148
|
path: z.string(),
|
|
@@ -190,7 +188,7 @@ This ensures optimal performance and context recovery.`,
|
|
|
190
188
|
|
|
191
189
|
server.tool(
|
|
192
190
|
'smart_shell',
|
|
193
|
-
'Run a diagnostic shell command from an allowlist. Allowed: pwd, ls, find, rg, git (status/diff/show/log/branch/rev-parse), npm/pnpm/yarn/bun (test/run/lint/build/typecheck/check). Blocks shell operators, pipes, and unsafe commands. Includes a unified confidence block: { blocked, timedOut }.',
|
|
191
|
+
'Run a diagnostic shell command from an allowlist. Allowed: pwd, ls, find, rg, git (status/diff/show/log/branch/rev-parse), npm/pnpm/yarn/bun (test/run/lint/build/typecheck/check). Blocks shell operators, pipes, and unsafe commands. For large diffs: output is split by file (up to 8 files, 60 lines each) with a hint to run git show -- <file> for the full body of any truncated file; prefer git diff --stat first to see which files changed, then git show -- <file> per file for targeted reading. Includes a unified confidence block: { blocked, timedOut }.',
|
|
194
192
|
{
|
|
195
193
|
command: z.string(),
|
|
196
194
|
},
|
|
@@ -487,7 +485,7 @@ This ensures optimal performance and context recovery.`,
|
|
|
487
485
|
|
|
488
486
|
server.tool(
|
|
489
487
|
'smart_turn',
|
|
490
|
-
'Orchestrate start/end of a meaningful agent turn
|
|
488
|
+
'Orchestrate start/end of a meaningful agent turn for multi-session tasks where context continuity matters. SKIP for single-session point-in-time tasks (reviewing a specific commit, answering a quick question, one-off lookup) — the setup overhead exceeds the benefit if the session will never be resumed. USE when: the task spans multiple chat sessions, you may return to it the next day, or the codebase context is large enough that re-reading is expensive. `phase: "start"` rehydrates persisted context, classifies prompt continuity against the saved session, optionally auto-creates a planning session for a new substantial task, returns `recommendedPath` guidance for the next safe devctx actions, and can include compact metrics. `phase: "end"` writes a checkpoint through smart_summary, returns follow-up `recommendedPath` guidance, and can optionally include compact metrics. Both phases expose `mutationSafety` when repo-safety blocks persisted writes and surface `storageHealth` when SQLite state is missing, oversized, locked, or corrupted.',
|
|
491
489
|
{
|
|
492
490
|
phase: z.enum(['start', 'end']),
|
|
493
491
|
sessionId: z.string().optional(),
|
|
@@ -412,17 +412,11 @@ export const smartContext = async ({
|
|
|
412
412
|
|
|
413
413
|
await ensureIndexReady({ root });
|
|
414
414
|
|
|
415
|
-
// Get detailed diff stats
|
|
416
415
|
const detailedChanges = await getDetailedDiff(changed.ref, root);
|
|
417
416
|
const index = loadIndex(root);
|
|
418
|
-
|
|
419
|
-
// Analyze impact and prioritize
|
|
420
417
|
const prioritized = analyzeChangeImpact(detailedChanges, index);
|
|
421
|
-
|
|
422
|
-
// Expand to include related files (importers, dependencies, tests)
|
|
423
418
|
const expandedFiles = expandChangedContext(changed.files, index, 10);
|
|
424
|
-
|
|
425
|
-
// Build primary seeds with priority and impact data
|
|
419
|
+
|
|
426
420
|
primarySeeds = Array.from(expandedFiles).map(rel => {
|
|
427
421
|
const changeInfo = prioritized.find(c => c.file === rel);
|
|
428
422
|
const evidence = [{
|
|
@@ -432,7 +426,6 @@ export const smartContext = async ({
|
|
|
432
426
|
impact: changeInfo?.impactScore || 0,
|
|
433
427
|
}];
|
|
434
428
|
|
|
435
|
-
// Mark files that were expanded (not directly changed)
|
|
436
429
|
if (!changed.files.includes(rel)) {
|
|
437
430
|
evidence[0].expanded = true;
|
|
438
431
|
}
|
|
@@ -444,7 +437,6 @@ export const smartContext = async ({
|
|
|
444
437
|
};
|
|
445
438
|
});
|
|
446
439
|
|
|
447
|
-
// Sort by impact (critical changes first)
|
|
448
440
|
primarySeeds.sort((a, b) => {
|
|
449
441
|
const impactA = a.evidence[0].impact || 0;
|
|
450
442
|
const impactB = b.evidence[0].impact || 0;
|
|
@@ -771,17 +763,13 @@ export const smartContext = async ({
|
|
|
771
763
|
timestamp: new Date().toISOString(),
|
|
772
764
|
});
|
|
773
765
|
|
|
774
|
-
// Record usage for feedback
|
|
775
766
|
recordToolUsage({
|
|
776
767
|
tool: 'smart_context',
|
|
777
768
|
savedTokens,
|
|
778
769
|
target: task,
|
|
779
770
|
});
|
|
780
|
-
|
|
781
|
-
// Record devctx operation for missed opportunity detection
|
|
782
771
|
recordDevctxOperation();
|
|
783
|
-
|
|
784
|
-
// Record decision explanation
|
|
772
|
+
|
|
785
773
|
let reason = DECISION_REASONS.TASK_CONTEXT;
|
|
786
774
|
if (diff) {
|
|
787
775
|
reason = DECISION_REASONS.DIFF_ANALYSIS;
|
package/src/tools/smart-read.js
CHANGED
|
@@ -536,17 +536,12 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
|
|
|
536
536
|
|
|
537
537
|
await persistMetrics(metrics);
|
|
538
538
|
|
|
539
|
-
// Record usage for feedback
|
|
540
539
|
recordToolUsage({
|
|
541
540
|
tool: 'smart_read',
|
|
542
541
|
savedTokens: metrics.savedTokens,
|
|
543
542
|
target: path.relative(effectiveRoot, fullPath),
|
|
544
543
|
});
|
|
545
|
-
|
|
546
|
-
// Record devctx operation for missed opportunity detection
|
|
547
544
|
recordDevctxOperation();
|
|
548
|
-
|
|
549
|
-
// Record decision explanation
|
|
550
545
|
const lineCount = content.split('\n').length;
|
|
551
546
|
let reason = DECISION_REASONS.LARGE_FILE;
|
|
552
547
|
let expectedBenefit = EXPECTED_BENEFITS.TOKEN_SAVINGS(metrics.savedTokens);
|
|
@@ -99,39 +99,35 @@ const parseRgLine = (line, root) => {
|
|
|
99
99
|
|
|
100
100
|
const MAX_FILE_SIZE = '1M';
|
|
101
101
|
|
|
102
|
-
const
|
|
102
|
+
const buildRgBaseArgs = () => {
|
|
103
103
|
const args = [
|
|
104
104
|
'--line-number',
|
|
105
105
|
'--no-heading',
|
|
106
|
-
'--color',
|
|
107
|
-
'never',
|
|
106
|
+
'--color', 'never',
|
|
108
107
|
'--smart-case',
|
|
109
|
-
'--fixed-strings',
|
|
110
108
|
'--max-filesize', MAX_FILE_SIZE,
|
|
111
109
|
];
|
|
112
|
-
|
|
113
110
|
for (const dir of ignoredDirs) {
|
|
114
111
|
args.push('--glob', `!${dir}/**`);
|
|
115
112
|
args.push('--glob', `!**/${dir}/**`);
|
|
116
113
|
}
|
|
117
|
-
|
|
118
114
|
for (const fileName of ignoredFileNames) {
|
|
119
115
|
args.push('--glob', `!${fileName}`);
|
|
120
116
|
}
|
|
121
|
-
|
|
122
117
|
for (const extension of supportedGlobs) {
|
|
123
118
|
args.push('--glob', extension);
|
|
124
119
|
}
|
|
120
|
+
return args;
|
|
121
|
+
};
|
|
125
122
|
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
const runRg = async (root, pattern, extraArgs = []) => {
|
|
124
|
+
const args = [...buildRgBaseArgs(), ...extraArgs, pattern, '.'];
|
|
128
125
|
try {
|
|
129
126
|
const { stdout } = await execFile(rgPath, args, {
|
|
130
127
|
cwd: root,
|
|
131
128
|
maxBuffer: 1024 * 1024 * 10,
|
|
132
129
|
timeout: 15000,
|
|
133
130
|
});
|
|
134
|
-
|
|
135
131
|
return stdout
|
|
136
132
|
.split('\n')
|
|
137
133
|
.filter(Boolean)
|
|
@@ -140,11 +136,47 @@ const searchWithRipgrep = async (root, query) => {
|
|
|
140
136
|
.filter((match) => !shouldIgnoreFile(match.file));
|
|
141
137
|
} catch (error) {
|
|
142
138
|
if (error.code === 1) return [];
|
|
143
|
-
|
|
139
|
+
process.stderr.write(`[smart-search] ripgrep failed: ${error.message}\n`);
|
|
144
140
|
return null;
|
|
145
141
|
}
|
|
146
142
|
};
|
|
147
143
|
|
|
144
|
+
const extractTerms = (query) =>
|
|
145
|
+
query
|
|
146
|
+
.split(/[\s,;|/\\]+/)
|
|
147
|
+
.map((t) => t.trim())
|
|
148
|
+
.filter((t) => t.length >= 3);
|
|
149
|
+
|
|
150
|
+
const searchWithRipgrep = async (root, query) => {
|
|
151
|
+
// Pass 1: exact literal match
|
|
152
|
+
const exact = await runRg(root, query, ['--fixed-strings']);
|
|
153
|
+
if (exact === null) return null;
|
|
154
|
+
if (exact.length > 0) return { matches: exact, searchMode: 'exact' };
|
|
155
|
+
|
|
156
|
+
// Pass 2: regex (handles partial words, snake_case, camelCase fragments)
|
|
157
|
+
const escaped = query.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&');
|
|
158
|
+
const regex = await runRg(root, escaped);
|
|
159
|
+
if (regex === null) return null;
|
|
160
|
+
if (regex.length > 0) return { matches: regex, searchMode: 'regex' };
|
|
161
|
+
|
|
162
|
+
// Pass 3: term expansion — search each significant word independently and merge
|
|
163
|
+
const terms = extractTerms(query);
|
|
164
|
+
if (terms.length < 2) return { matches: [], searchMode: 'exact', zeroReason: 'no_matches' };
|
|
165
|
+
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
const merged = [];
|
|
168
|
+
for (const term of terms) {
|
|
169
|
+
const hits = await runRg(root, term, ['--fixed-strings']);
|
|
170
|
+
if (!hits) continue;
|
|
171
|
+
for (const hit of hits) {
|
|
172
|
+
const key = `${hit.file}:${hit.lineNumber}`;
|
|
173
|
+
if (!seen.has(key)) { seen.add(key); merged.push({ ...hit, matchedTerm: term }); }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { matches: merged, searchMode: 'terms', terms };
|
|
178
|
+
};
|
|
179
|
+
|
|
148
180
|
const MAX_FALLBACK_FILE_BYTES = 1024 * 1024;
|
|
149
181
|
|
|
150
182
|
export const isSmartCaseSensitive = (query) => query !== query.toLowerCase();
|
|
@@ -273,16 +305,43 @@ const groupMatches = (matches, query, intent, indexHits, graphHits) => {
|
|
|
273
305
|
return { groups: sorted, breakdown };
|
|
274
306
|
};
|
|
275
307
|
|
|
276
|
-
const
|
|
308
|
+
const buildZeroResultsMessage = (query, searchMode, provenance) => {
|
|
309
|
+
const lines = [`No matches found for: "${query}"`];
|
|
310
|
+
|
|
311
|
+
if (searchMode === 'exact') {
|
|
312
|
+
lines.push('• Tried: exact literal match (--fixed-strings)');
|
|
313
|
+
lines.push('• Tried: regex match');
|
|
314
|
+
} else if (searchMode === 'terms') {
|
|
315
|
+
const terms = provenance?.expandedTerms ?? [];
|
|
316
|
+
lines.push(`• Tried: exact, regex, and term expansion (${terms.join(', ')})`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
lines.push('');
|
|
320
|
+
lines.push('Suggestions:');
|
|
321
|
+
lines.push(' – Use a shorter, more specific term (e.g. a function name, not a phrase)');
|
|
322
|
+
lines.push(' – Try Grep for raw text: the query may be in a file type not indexed by smart_search');
|
|
323
|
+
lines.push(' – Run build_index to enable symbol-level search if the codebase is new');
|
|
324
|
+
|
|
325
|
+
return lines.join('\n');
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const buildCompactResult = (groups, totalMatches, query, root, searchMode, provenance) => {
|
|
329
|
+
if (totalMatches === 0) {
|
|
330
|
+
return buildZeroResultsMessage(query, searchMode, provenance);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const modeLabel = searchMode === 'exact' ? '' : searchMode === 'regex' ? ' [regex fallback]' : ` [term expansion: ${(provenance?.expandedTerms ?? []).join(', ')}]`;
|
|
334
|
+
|
|
277
335
|
if (totalMatches <= 20) {
|
|
278
|
-
|
|
336
|
+
const header = modeLabel ? `# Search mode:${modeLabel}\n\n` : '';
|
|
337
|
+
return header + groups
|
|
279
338
|
.flatMap((group) => group.matches)
|
|
280
339
|
.map(formatMatch)
|
|
281
340
|
.join('\n');
|
|
282
341
|
}
|
|
283
342
|
|
|
284
343
|
const lines = [
|
|
285
|
-
`query: ${query}`,
|
|
344
|
+
`query: ${query}${modeLabel}`,
|
|
286
345
|
`root: ${root}`,
|
|
287
346
|
`total matches: ${totalMatches}`,
|
|
288
347
|
`matched files: ${groups.length}`,
|
|
@@ -314,12 +373,13 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
314
373
|
}
|
|
315
374
|
|
|
316
375
|
const root = resolveSafePath(cwd);
|
|
317
|
-
const
|
|
318
|
-
const usedFallback =
|
|
376
|
+
const rgResult = _testForceWalk ? null : await searchWithRipgrep(root, query);
|
|
377
|
+
const usedFallback = rgResult === null;
|
|
319
378
|
const engine = usedFallback ? 'walk' : 'rg';
|
|
320
379
|
|
|
321
380
|
let rawMatches;
|
|
322
381
|
let provenance;
|
|
382
|
+
let searchMode = 'exact';
|
|
323
383
|
|
|
324
384
|
if (usedFallback) {
|
|
325
385
|
const fallback = searchWithFallback(root, query);
|
|
@@ -340,7 +400,9 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
340
400
|
warnings,
|
|
341
401
|
};
|
|
342
402
|
} else {
|
|
343
|
-
rawMatches =
|
|
403
|
+
rawMatches = rgResult.matches;
|
|
404
|
+
searchMode = rgResult.searchMode;
|
|
405
|
+
if (rgResult.terms) provenance = { expandedTerms: rgResult.terms };
|
|
344
406
|
}
|
|
345
407
|
|
|
346
408
|
rawMatches = rawMatches.filter((match) => !shouldIgnoreFile(match.file));
|
|
@@ -403,7 +465,7 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
403
465
|
}
|
|
404
466
|
|
|
405
467
|
const rawText = dedupedMatches.map(formatMatch).join('\n');
|
|
406
|
-
const compressedText = truncate(buildCompactResult(groups, dedupedMatches.length, query, root), 5000);
|
|
468
|
+
const compressedText = truncate(buildCompactResult(groups, dedupedMatches.length, query, root, searchMode, provenance), 5000);
|
|
407
469
|
const metrics = buildMetrics({
|
|
408
470
|
tool: 'smart_search',
|
|
409
471
|
target: `${root} :: ${query}`,
|
|
@@ -413,17 +475,13 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
413
475
|
|
|
414
476
|
await persistMetrics(metrics);
|
|
415
477
|
|
|
416
|
-
// Record usage for feedback
|
|
417
478
|
recordToolUsage({
|
|
418
479
|
tool: 'smart_search',
|
|
419
480
|
savedTokens: metrics.savedTokens,
|
|
420
481
|
target: query,
|
|
421
482
|
});
|
|
422
|
-
|
|
423
|
-
// Record devctx operation for missed opportunity detection
|
|
424
483
|
recordDevctxOperation();
|
|
425
|
-
|
|
426
|
-
// Record decision explanation
|
|
484
|
+
|
|
427
485
|
let reason = DECISION_REASONS.MULTIPLE_FILES;
|
|
428
486
|
if (validIntent) {
|
|
429
487
|
reason = DECISION_REASONS.INTENT_AWARE;
|
|
@@ -442,9 +500,11 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
|
|
|
442
500
|
});
|
|
443
501
|
|
|
444
502
|
let retrievalConfidence = 'high';
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
|
|
503
|
+
if (dedupedMatches.length === 0) retrievalConfidence = 'none';
|
|
504
|
+
else if (searchMode === 'terms') retrievalConfidence = 'low';
|
|
505
|
+
else if (searchMode === 'regex') retrievalConfidence = 'medium';
|
|
506
|
+
else if (usedFallback) retrievalConfidence = provenance?.skippedItemsTotal > 0 ? 'low' : 'medium';
|
|
507
|
+
else if (provenance?.skippedItemsTotal > 0) retrievalConfidence = 'low';
|
|
448
508
|
|
|
449
509
|
const confidence = { level: retrievalConfidence, indexFreshness };
|
|
450
510
|
|
package/src/tools/smart-shell.js
CHANGED
|
@@ -176,6 +176,52 @@ const validateCommand = (command, tokens) => {
|
|
|
176
176
|
return null;
|
|
177
177
|
};
|
|
178
178
|
|
|
179
|
+
const DIFF_FILE_HEADER = /^diff --git a\/.+ b\/.+/;
|
|
180
|
+
const DIFF_HUNK_HEADER = /^@@ /;
|
|
181
|
+
const MAX_DIFF_FILES = 8;
|
|
182
|
+
const MAX_LINES_PER_FILE = 60;
|
|
183
|
+
const DIFF_TOTAL_LIMIT = 4000;
|
|
184
|
+
|
|
185
|
+
const splitDiffByFile = (text) => {
|
|
186
|
+
const files = [];
|
|
187
|
+
let current = null;
|
|
188
|
+
|
|
189
|
+
for (const line of text.split('\n')) {
|
|
190
|
+
if (DIFF_FILE_HEADER.test(line)) {
|
|
191
|
+
if (current) files.push(current);
|
|
192
|
+
current = { header: line, lines: [] };
|
|
193
|
+
} else if (current) {
|
|
194
|
+
current.lines.push(line);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (current) files.push(current);
|
|
198
|
+
return files;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const compressDiff = (text) => {
|
|
202
|
+
if (!DIFF_FILE_HEADER.test(text)) return text;
|
|
203
|
+
|
|
204
|
+
const files = splitDiffByFile(text);
|
|
205
|
+
if (files.length === 0) return text;
|
|
206
|
+
|
|
207
|
+
const shown = files.slice(0, MAX_DIFF_FILES);
|
|
208
|
+
const skipped = files.length - shown.length;
|
|
209
|
+
|
|
210
|
+
const parts = shown.map(({ header, lines }) => {
|
|
211
|
+
const truncatedLines = lines.slice(0, MAX_LINES_PER_FILE);
|
|
212
|
+
const skippedLines = lines.length - truncatedLines.length;
|
|
213
|
+
const hunkCount = lines.filter((l) => DIFF_HUNK_HEADER.test(l)).length;
|
|
214
|
+
const suffix = skippedLines > 0 ? [`... (${skippedLines} more lines — use smart_read(symbol) for full body)`] : [];
|
|
215
|
+
return [header, `# ${hunkCount} hunk(s)`, ...truncatedLines, ...suffix].join('\n');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const footer = skipped > 0
|
|
219
|
+
? `\n# ${skipped} more file(s) not shown — run git show -- <file> for each`
|
|
220
|
+
: '';
|
|
221
|
+
|
|
222
|
+
return truncate(parts.join('\n\n'), DIFF_TOTAL_LIMIT) + footer;
|
|
223
|
+
};
|
|
224
|
+
|
|
179
225
|
const buildBlockedResult = async (command, message) => {
|
|
180
226
|
const metrics = buildMetrics({
|
|
181
227
|
tool: 'smart_shell',
|
|
@@ -254,7 +300,7 @@ export const smartShell = async ({ command }) => {
|
|
|
254
300
|
]);
|
|
255
301
|
const shouldPrioritizeRelevant = execution.code !== 0 || execution.timedOut;
|
|
256
302
|
const compressedSource = shouldPrioritizeRelevant && relevant ? relevant : rawText;
|
|
257
|
-
const compressedText = truncate(uniqueLines(compressedSource), 5000);
|
|
303
|
+
const compressedText = truncate(compressDiff(uniqueLines(compressedSource)), 5000);
|
|
258
304
|
const metrics = buildMetrics({
|
|
259
305
|
tool: 'smart_shell',
|
|
260
306
|
target: command,
|
|
@@ -264,17 +310,12 @@ export const smartShell = async ({ command }) => {
|
|
|
264
310
|
|
|
265
311
|
await persistMetrics(metrics);
|
|
266
312
|
|
|
267
|
-
// Record usage for feedback
|
|
268
313
|
recordToolUsage({
|
|
269
314
|
tool: 'smart_shell',
|
|
270
315
|
savedTokens: metrics.savedTokens,
|
|
271
316
|
target: command,
|
|
272
317
|
});
|
|
273
|
-
|
|
274
|
-
// Record devctx operation for missed opportunity detection
|
|
275
318
|
recordDevctxOperation();
|
|
276
|
-
|
|
277
|
-
// Record decision explanation
|
|
278
319
|
const outputLines = rawText.split('\n').length;
|
|
279
320
|
let reason = DECISION_REASONS.COMMAND_OUTPUT;
|
|
280
321
|
if (shouldPrioritizeRelevant && relevant) {
|
package/src/usage-feedback.js
CHANGED
|
@@ -60,7 +60,6 @@ export const formatUsageFeedback = () => {
|
|
|
60
60
|
lines.push('');
|
|
61
61
|
lines.push('📊 **devctx usage this session:**');
|
|
62
62
|
|
|
63
|
-
// Sort by count descending
|
|
64
63
|
const sorted = usage.tools.sort((a, b) => b.count - a.count);
|
|
65
64
|
|
|
66
65
|
for (const { tool, count, savedTokens, targets } of sorted) {
|
package/src/workflow-tracker.js
CHANGED
|
@@ -183,7 +183,6 @@ export const endWorkflow = (workflowId) => {
|
|
|
183
183
|
return null;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
// Get workflow start time and session
|
|
187
186
|
const workflow = db
|
|
188
187
|
.prepare(
|
|
189
188
|
`
|
|
@@ -203,7 +202,6 @@ export const endWorkflow = (workflowId) => {
|
|
|
203
202
|
const endTime = new Date(now);
|
|
204
203
|
const durationMs = endTime - startTime;
|
|
205
204
|
|
|
206
|
-
// Get all metrics for this session since workflow start
|
|
207
205
|
const metrics = db
|
|
208
206
|
.prepare(
|
|
209
207
|
`
|
|
@@ -215,7 +213,6 @@ export const endWorkflow = (workflowId) => {
|
|
|
215
213
|
)
|
|
216
214
|
.all(workflow.session_id, workflow.start_time);
|
|
217
215
|
|
|
218
|
-
// Calculate totals
|
|
219
216
|
const rawTokens = metrics.reduce((sum, m) => sum + (m.raw_tokens || 0), 0);
|
|
220
217
|
const compressedTokens = metrics.reduce((sum, m) => sum + (m.compressed_tokens || 0), 0);
|
|
221
218
|
const savedTokens = metrics.reduce((sum, m) => sum + (m.saved_tokens || 0), 0);
|
|
@@ -231,7 +228,6 @@ export const endWorkflow = (workflowId) => {
|
|
|
231
228
|
const savingsPct = rawTokens > 0 ? ((savedTokens / rawTokens) * 100).toFixed(2) : 0;
|
|
232
229
|
const netSavingsPct = rawTokens > 0 ? ((netSavedTokens / rawTokens) * 100).toFixed(2) : 0;
|
|
233
230
|
|
|
234
|
-
// Calculate vs baseline
|
|
235
231
|
const baselineTokens = workflow.baseline_tokens || 0;
|
|
236
232
|
const vsBaselinePct = baselineTokens > 0 ? (((baselineTokens - compressedTokens) / baselineTokens) * 100).toFixed(2) : 0;
|
|
237
233
|
const vsBaselineNetPct = baselineTokens > 0 ? (((baselineTokens - (compressedTokens + overheadTokens)) / baselineTokens) * 100).toFixed(2) : 0;
|
|
@@ -247,10 +243,7 @@ export const endWorkflow = (workflowId) => {
|
|
|
247
243
|
},
|
|
248
244
|
};
|
|
249
245
|
|
|
250
|
-
// Get unique tools used
|
|
251
246
|
const toolsUsed = [...new Set(metrics.map((m) => m.tool))];
|
|
252
|
-
|
|
253
|
-
// Update workflow
|
|
254
247
|
const stmt = db.prepare(`
|
|
255
248
|
UPDATE workflow_metrics
|
|
256
249
|
SET end_time = ?,
|
|
@@ -584,7 +577,6 @@ export const autoTrackWorkflow = (sessionId, sessionGoal) => {
|
|
|
584
577
|
return null;
|
|
585
578
|
}
|
|
586
579
|
|
|
587
|
-
// Check if workflow already tracked for this session
|
|
588
580
|
const existing = db
|
|
589
581
|
.prepare(
|
|
590
582
|
`
|
|
@@ -601,7 +593,6 @@ export const autoTrackWorkflow = (sessionId, sessionGoal) => {
|
|
|
601
593
|
return existing.workflow_id;
|
|
602
594
|
}
|
|
603
595
|
|
|
604
|
-
// Get tools used so far in this session
|
|
605
596
|
const metrics = db
|
|
606
597
|
.prepare(
|
|
607
598
|
`
|
|
@@ -621,7 +612,6 @@ export const autoTrackWorkflow = (sessionId, sessionGoal) => {
|
|
|
621
612
|
return null;
|
|
622
613
|
}
|
|
623
614
|
|
|
624
|
-
// Start tracking
|
|
625
615
|
return startWorkflow(workflowType, sessionId, { autoDetected: true, goal: sessionGoal });
|
|
626
616
|
});
|
|
627
617
|
} catch {
|