smart-context-mcp 0.8.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/LICENSE +21 -0
- package/README.md +414 -0
- package/package.json +63 -0
- package/scripts/devctx-server.js +4 -0
- package/scripts/init-clients.js +356 -0
- package/scripts/report-metrics.js +195 -0
- package/src/index.js +976 -0
- package/src/mcp-server.js +3 -0
- package/src/metrics.js +65 -0
- package/src/server.js +143 -0
- package/src/tokenCounter.js +12 -0
- package/src/tools/smart-context.js +1192 -0
- package/src/tools/smart-read/additional-languages.js +684 -0
- package/src/tools/smart-read/code.js +216 -0
- package/src/tools/smart-read/fallback.js +23 -0
- package/src/tools/smart-read/python.js +178 -0
- package/src/tools/smart-read/shared.js +39 -0
- package/src/tools/smart-read/structured.js +72 -0
- package/src/tools/smart-read-batch.js +63 -0
- package/src/tools/smart-read.js +459 -0
- package/src/tools/smart-search.js +412 -0
- package/src/tools/smart-shell.js +213 -0
- package/src/utils/fs.js +47 -0
- package/src/utils/paths.js +1 -0
- package/src/utils/runtime-config.js +29 -0
- package/src/utils/text.js +38 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
7
|
+
const scriptsDir = path.dirname(currentFilePath);
|
|
8
|
+
const devctxDir = path.resolve(scriptsDir, '..');
|
|
9
|
+
const supportedClients = new Set(['cursor', 'codex', 'qwen', 'claude']);
|
|
10
|
+
|
|
11
|
+
const requireValue = (argv, index, flag) => {
|
|
12
|
+
const value = argv[index + 1];
|
|
13
|
+
if (!value || value.startsWith('--')) {
|
|
14
|
+
throw new Error(`Missing value for ${flag}`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const parseArgs = (argv) => {
|
|
20
|
+
const options = {
|
|
21
|
+
target: process.cwd(),
|
|
22
|
+
name: 'devctx',
|
|
23
|
+
command: 'node',
|
|
24
|
+
args: null,
|
|
25
|
+
clients: [...supportedClients],
|
|
26
|
+
dryRun: false,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
30
|
+
const token = argv[index];
|
|
31
|
+
|
|
32
|
+
if (token === '--target') {
|
|
33
|
+
options.target = requireValue(argv, index, '--target');
|
|
34
|
+
index += 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (token === '--name') {
|
|
39
|
+
options.name = requireValue(argv, index, '--name');
|
|
40
|
+
index += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (token === '--command') {
|
|
45
|
+
options.command = requireValue(argv, index, '--command');
|
|
46
|
+
index += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (token === '--args') {
|
|
51
|
+
const raw = requireValue(argv, index, '--args');
|
|
52
|
+
try {
|
|
53
|
+
options.args = JSON.parse(raw);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Error('--args must be valid JSON');
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(options.args)) {
|
|
58
|
+
throw new Error('--args must be a JSON array');
|
|
59
|
+
}
|
|
60
|
+
index += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (token === '--clients') {
|
|
65
|
+
options.clients = requireValue(argv, index, '--clients')
|
|
66
|
+
.split(',')
|
|
67
|
+
.map((value) => value.trim().toLowerCase())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
index += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (token === '--dry-run') {
|
|
74
|
+
options.dryRun = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw new Error(`Unknown argument: ${token}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const invalidClients = options.clients.filter((client) => !supportedClients.has(client));
|
|
82
|
+
|
|
83
|
+
if (invalidClients.length > 0) {
|
|
84
|
+
throw new Error(`Unsupported clients: ${invalidClients.join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return options;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const normalizeCommandPath = (value) => {
|
|
91
|
+
if (path.isAbsolute(value) || value.startsWith('./') || value.startsWith('../')) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return `./${value}`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const readJson = (filePath, fallback) => {
|
|
99
|
+
if (!fs.existsSync(filePath)) {
|
|
100
|
+
return fallback;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
105
|
+
} catch {
|
|
106
|
+
throw new Error(`Invalid JSON in ${filePath}`);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const writeFile = (filePath, content, dryRun) => {
|
|
111
|
+
if (dryRun) {
|
|
112
|
+
console.log(`[dry-run] write ${filePath}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
117
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
118
|
+
console.log(`updated ${filePath}`);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const getServerConfig = ({ name, command, args }) => ({
|
|
122
|
+
name,
|
|
123
|
+
config: {
|
|
124
|
+
command,
|
|
125
|
+
args,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const updateCursorConfig = (targetDir, serverConfig, dryRun) => {
|
|
130
|
+
const filePath = path.join(targetDir, '.cursor', 'mcp.json');
|
|
131
|
+
const current = readJson(filePath, { mcpServers: {} });
|
|
132
|
+
current.mcpServers ??= {};
|
|
133
|
+
current.mcpServers[serverConfig.name] = serverConfig.config;
|
|
134
|
+
writeFile(filePath, `${JSON.stringify(current, null, 2)}\n`, dryRun);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const updateClaudeConfig = (targetDir, serverConfig, dryRun) => {
|
|
138
|
+
const filePath = path.join(targetDir, '.mcp.json');
|
|
139
|
+
const current = readJson(filePath, { mcpServers: {} });
|
|
140
|
+
current.mcpServers ??= {};
|
|
141
|
+
current.mcpServers[serverConfig.name] = {
|
|
142
|
+
...serverConfig.config,
|
|
143
|
+
env: current.mcpServers[serverConfig.name]?.env ?? {},
|
|
144
|
+
};
|
|
145
|
+
writeFile(filePath, `${JSON.stringify(current, null, 2)}\n`, dryRun);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const updateQwenConfig = (targetDir, serverConfig, dryRun) => {
|
|
149
|
+
const filePath = path.join(targetDir, '.qwen', 'settings.json');
|
|
150
|
+
const current = readJson(filePath, {});
|
|
151
|
+
current.mcp ??= {};
|
|
152
|
+
current.mcp.enabled = true;
|
|
153
|
+
current.mcpServers ??= {};
|
|
154
|
+
current.mcpServers[serverConfig.name] = serverConfig.config;
|
|
155
|
+
writeFile(filePath, `${JSON.stringify(current, null, 2)}\n`, dryRun);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const buildCodexSection = (serverConfig) => {
|
|
159
|
+
const header = `[mcp_servers.${serverConfig.name}]`;
|
|
160
|
+
const body = [
|
|
161
|
+
'enabled = true',
|
|
162
|
+
'required = false',
|
|
163
|
+
`command = ${JSON.stringify(serverConfig.config.command)}`,
|
|
164
|
+
`args = [${serverConfig.config.args.map((value) => JSON.stringify(value)).join(', ')}]`,
|
|
165
|
+
'startup_timeout_sec = 15.0',
|
|
166
|
+
'tool_timeout_sec = 30.0',
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
return { header, body };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const upsertTomlSection = (content, header, bodyLines) => {
|
|
173
|
+
const lines = content.split('\n');
|
|
174
|
+
const nextLines = [];
|
|
175
|
+
let skipping = false;
|
|
176
|
+
let found = false;
|
|
177
|
+
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
const trimmed = line.trim();
|
|
180
|
+
|
|
181
|
+
if (!skipping && trimmed === header) {
|
|
182
|
+
found = true;
|
|
183
|
+
skipping = true;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (skipping) {
|
|
188
|
+
if (trimmed.startsWith('[') && trimmed !== header) {
|
|
189
|
+
skipping = false;
|
|
190
|
+
nextLines.push(line);
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
nextLines.push(line);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const preserved = nextLines.join('\n').trim();
|
|
199
|
+
const section = [header, ...bodyLines].join('\n');
|
|
200
|
+
|
|
201
|
+
if (!found && preserved.length === 0) {
|
|
202
|
+
return `${section}\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!found) {
|
|
206
|
+
return `${preserved}\n\n${section}\n`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (preserved.length === 0) {
|
|
210
|
+
return `${section}\n`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return `${preserved}\n\n${section}\n`;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const updateCodexConfig = (targetDir, serverConfig, dryRun) => {
|
|
217
|
+
const filePath = path.join(targetDir, '.codex', 'config.toml');
|
|
218
|
+
const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
219
|
+
const section = buildCodexSection(serverConfig);
|
|
220
|
+
const nextContent = upsertTomlSection(current, section.header, section.body);
|
|
221
|
+
writeFile(filePath, nextContent, dryRun);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Agent rules — instruct agents to prefer devctx tools over built-in ones
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
const agentRuleBody = `Always prefer devctx MCP tools over built-in file reading, code search, and diagnostics.
|
|
229
|
+
|
|
230
|
+
- Use smart_read outline or signatures before full (~90% token savings).
|
|
231
|
+
- Use smart_read symbol (string or array) to extract specific functions/classes before editing.
|
|
232
|
+
- Use smart_read range for specific lines when you know the location.
|
|
233
|
+
- Use full mode only when outline/signatures/symbol are insufficient.
|
|
234
|
+
- Use smart_search instead of grep/ripgrep — it groups, ranks, and filters automatically.
|
|
235
|
+
- Pass intent to smart_search to get task-aware ranking (implementation/debug/tests/config/docs/explore).
|
|
236
|
+
- Use smart_shell for diagnostics: git status, ls, find, pwd, test output.
|
|
237
|
+
|
|
238
|
+
By task:
|
|
239
|
+
- Debugging: smart_search with intent=debug → read signatures → inspect symbol → smart_shell for tests/errors.
|
|
240
|
+
- Review: smart_search with intent=implementation → read outline/signatures, focus on changed symbols, minimal changes.
|
|
241
|
+
- Refactor: smart_search with intent=implementation → signatures for public API, preserve behavior, small edits, verify with tests.
|
|
242
|
+
- Tests: smart_search with intent=tests → find existing tests, read symbol of function under test.
|
|
243
|
+
- Config: smart_search with intent=config → find settings, env vars, infrastructure files.
|
|
244
|
+
- Architecture: smart_search with intent=explore → directory structure, outlines of key modules and API boundaries.`;
|
|
245
|
+
|
|
246
|
+
const cursorRuleContent = `---
|
|
247
|
+
description: Prefer devctx MCP tools for file reading, code search, and diagnostics
|
|
248
|
+
alwaysApply: true
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
${agentRuleBody}
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
const updateCursorRule = (targetDir, dryRun) => {
|
|
255
|
+
const filePath = path.join(targetDir, '.cursor', 'rules', 'devctx.mdc');
|
|
256
|
+
writeFile(filePath, cursorRuleContent, dryRun);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const SECTION_START = '<!-- devctx:start -->';
|
|
260
|
+
const SECTION_END = '<!-- devctx:end -->';
|
|
261
|
+
|
|
262
|
+
const markdownSection = `${SECTION_START}
|
|
263
|
+
## devctx
|
|
264
|
+
|
|
265
|
+
${agentRuleBody}
|
|
266
|
+
${SECTION_END}`;
|
|
267
|
+
|
|
268
|
+
const upsertMarkdownSection = (content) => {
|
|
269
|
+
const startIdx = content.indexOf(SECTION_START);
|
|
270
|
+
const endIdx = content.indexOf(SECTION_END);
|
|
271
|
+
|
|
272
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
273
|
+
return content.slice(0, startIdx) + markdownSection + content.slice(endIdx + SECTION_END.length);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const trimmed = content.trimEnd();
|
|
277
|
+
return trimmed.length === 0 ? `${markdownSection}\n` : `${trimmed}\n\n${markdownSection}\n`;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const updateAgentsMd = (targetDir, dryRun) => {
|
|
281
|
+
const filePath = path.join(targetDir, 'AGENTS.md');
|
|
282
|
+
const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
283
|
+
writeFile(filePath, upsertMarkdownSection(current), dryRun);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const updateClaudeMd = (targetDir, dryRun) => {
|
|
287
|
+
const filePath = path.join(targetDir, 'CLAUDE.md');
|
|
288
|
+
const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
289
|
+
writeFile(filePath, upsertMarkdownSection(current), dryRun);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const hasGitignoreEntry = (content, entry) => {
|
|
293
|
+
const target = entry.replace(/\/+$/, '');
|
|
294
|
+
return content
|
|
295
|
+
.split(/\r?\n/)
|
|
296
|
+
.map((line) => line.trim())
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.map((line) => line.replace(/\/+$/, ''))
|
|
299
|
+
.includes(target);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const ensureGitignoreEntry = (targetDir, dryRun) => {
|
|
303
|
+
const filePath = path.join(targetDir, '.gitignore');
|
|
304
|
+
const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
305
|
+
|
|
306
|
+
if (hasGitignoreEntry(current, '.devctx/')) return;
|
|
307
|
+
|
|
308
|
+
const trimmed = current.trimEnd();
|
|
309
|
+
const next = trimmed.length === 0 ? '.devctx/\n' : `${trimmed}\n\n.devctx/\n`;
|
|
310
|
+
writeFile(filePath, next, dryRun);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const main = () => {
|
|
314
|
+
const options = parseArgs(process.argv.slice(2));
|
|
315
|
+
const targetDir = path.resolve(options.target);
|
|
316
|
+
const defaultArgs = [normalizeCommandPath(path.relative(targetDir, path.join(devctxDir, 'src', 'mcp-server.js')))];
|
|
317
|
+
const args = options.args ?? defaultArgs;
|
|
318
|
+
const serverConfig = getServerConfig({
|
|
319
|
+
name: options.name,
|
|
320
|
+
command: options.command,
|
|
321
|
+
args,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const clientSet = new Set(options.clients);
|
|
325
|
+
ensureGitignoreEntry(targetDir, options.dryRun);
|
|
326
|
+
|
|
327
|
+
if (clientSet.has('cursor')) {
|
|
328
|
+
updateCursorConfig(targetDir, serverConfig, options.dryRun);
|
|
329
|
+
updateCursorRule(targetDir, options.dryRun);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (clientSet.has('codex')) {
|
|
333
|
+
updateCodexConfig(targetDir, serverConfig, options.dryRun);
|
|
334
|
+
updateAgentsMd(targetDir, options.dryRun);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (clientSet.has('qwen')) {
|
|
338
|
+
updateQwenConfig(targetDir, serverConfig, options.dryRun);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (clientSet.has('claude')) {
|
|
342
|
+
updateClaudeConfig(targetDir, serverConfig, options.dryRun);
|
|
343
|
+
updateClaudeMd(targetDir, options.dryRun);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.log(`configured clients: ${[...clientSet].join(', ')}`);
|
|
347
|
+
console.log(`target: ${targetDir}`);
|
|
348
|
+
console.log(`command: ${serverConfig.config.command} ${serverConfig.config.args.join(' ')}`);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
main();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(error.message);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getLegacyMetricsFilePath, getMetricsFilePath } from '../src/metrics.js';
|
|
5
|
+
|
|
6
|
+
const requireValue = (argv, index, flag) => {
|
|
7
|
+
const value = argv[index + 1];
|
|
8
|
+
if (!value || value.startsWith('--')) {
|
|
9
|
+
throw new Error(`Missing value for ${flag}`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const parseArgs = (argv) => {
|
|
15
|
+
const options = {
|
|
16
|
+
file: null,
|
|
17
|
+
json: false,
|
|
18
|
+
tool: null,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
22
|
+
const token = argv[index];
|
|
23
|
+
|
|
24
|
+
if (token === '--file') {
|
|
25
|
+
options.file = path.resolve(requireValue(argv, index, '--file'));
|
|
26
|
+
index += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (token === '--tool') {
|
|
31
|
+
options.tool = requireValue(argv, index, '--tool');
|
|
32
|
+
index += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (token === '--json') {
|
|
37
|
+
options.json = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new Error(`Unknown argument: ${token}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return options;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const unique = (items) => [...new Set(items.filter(Boolean))];
|
|
48
|
+
|
|
49
|
+
const resolveMetricsInput = (options) => {
|
|
50
|
+
if (options.file) {
|
|
51
|
+
return { filePath: options.file, source: 'explicit' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const defaultPath = getMetricsFilePath();
|
|
55
|
+
const legacyPath = getLegacyMetricsFilePath();
|
|
56
|
+
const candidates = unique([defaultPath, legacyPath]);
|
|
57
|
+
const existing = candidates.find((filePath) => fs.existsSync(filePath));
|
|
58
|
+
|
|
59
|
+
if (existing) {
|
|
60
|
+
return {
|
|
61
|
+
filePath: existing,
|
|
62
|
+
source: existing === legacyPath ? 'legacy' : 'default',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { filePath: defaultPath, source: 'default' };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const readEntries = (filePath) => {
|
|
70
|
+
if (!fs.existsSync(filePath)) {
|
|
71
|
+
throw new Error(`No metrics file found at ${filePath}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const lines = fs.readFileSync(filePath, 'utf8')
|
|
75
|
+
.split('\n')
|
|
76
|
+
.map((line) => line.trim())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
|
|
79
|
+
const entries = [];
|
|
80
|
+
const invalidLines = [];
|
|
81
|
+
|
|
82
|
+
lines.forEach((line, index) => {
|
|
83
|
+
try {
|
|
84
|
+
entries.push(JSON.parse(line));
|
|
85
|
+
} catch {
|
|
86
|
+
invalidLines.push(index + 1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { entries, invalidLines };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const aggregate = (entries) => {
|
|
94
|
+
const byTool = new Map();
|
|
95
|
+
let rawTokens = 0;
|
|
96
|
+
let compressedTokens = 0;
|
|
97
|
+
let savedTokens = 0;
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
const tool = entry.tool ?? 'unknown';
|
|
101
|
+
const current = byTool.get(tool) ?? {
|
|
102
|
+
tool,
|
|
103
|
+
count: 0,
|
|
104
|
+
rawTokens: 0,
|
|
105
|
+
compressedTokens: 0,
|
|
106
|
+
savedTokens: 0,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
current.count += 1;
|
|
110
|
+
current.rawTokens += Number(entry.rawTokens ?? 0);
|
|
111
|
+
current.compressedTokens += Number(entry.compressedTokens ?? 0);
|
|
112
|
+
current.savedTokens += Number(entry.savedTokens ?? 0);
|
|
113
|
+
byTool.set(tool, current);
|
|
114
|
+
|
|
115
|
+
rawTokens += Number(entry.rawTokens ?? 0);
|
|
116
|
+
compressedTokens += Number(entry.compressedTokens ?? 0);
|
|
117
|
+
savedTokens += Number(entry.savedTokens ?? 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const tools = [...byTool.values()]
|
|
121
|
+
.map((item) => ({
|
|
122
|
+
...item,
|
|
123
|
+
savingsPct: item.rawTokens > 0 ? +((item.savedTokens / item.rawTokens) * 100).toFixed(2) : 0,
|
|
124
|
+
}))
|
|
125
|
+
.sort((a, b) => b.savedTokens - a.savedTokens || b.count - a.count || a.tool.localeCompare(b.tool));
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
count: entries.length,
|
|
129
|
+
rawTokens,
|
|
130
|
+
compressedTokens,
|
|
131
|
+
savedTokens,
|
|
132
|
+
savingsPct: rawTokens > 0 ? +((savedTokens / rawTokens) * 100).toFixed(2) : 0,
|
|
133
|
+
tools,
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const formatNumber = (value) => new Intl.NumberFormat('en-US').format(value);
|
|
138
|
+
|
|
139
|
+
const printHuman = (report) => {
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log('devctx metrics report');
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(`File: ${report.filePath}`);
|
|
144
|
+
console.log(`Source: ${report.source}`);
|
|
145
|
+
console.log(`Entries: ${formatNumber(report.summary.count)}`);
|
|
146
|
+
console.log(`Raw tokens: ${formatNumber(report.summary.rawTokens)}`);
|
|
147
|
+
console.log(`Final tokens: ${formatNumber(report.summary.compressedTokens)}`);
|
|
148
|
+
console.log(`Saved tokens: ${formatNumber(report.summary.savedTokens)} (${report.summary.savingsPct}%)`);
|
|
149
|
+
if (report.invalidLines.length > 0) {
|
|
150
|
+
console.log(`Invalid JSONL: ${report.invalidLines.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log('By tool:');
|
|
154
|
+
|
|
155
|
+
if (report.summary.tools.length === 0) {
|
|
156
|
+
console.log(' no entries');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const tool of report.summary.tools) {
|
|
161
|
+
console.log(
|
|
162
|
+
` ${tool.tool.padEnd(14)} count=${formatNumber(tool.count)} raw=${formatNumber(tool.rawTokens)} final=${formatNumber(tool.compressedTokens)} saved=${formatNumber(tool.savedTokens)} (${tool.savingsPct}%)`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const main = () => {
|
|
168
|
+
const options = parseArgs(process.argv.slice(2));
|
|
169
|
+
const resolved = resolveMetricsInput(options);
|
|
170
|
+
const { entries, invalidLines } = readEntries(resolved.filePath);
|
|
171
|
+
const filteredEntries = options.tool ? entries.filter((entry) => entry.tool === options.tool) : entries;
|
|
172
|
+
const summary = aggregate(filteredEntries);
|
|
173
|
+
|
|
174
|
+
const report = {
|
|
175
|
+
filePath: resolved.filePath,
|
|
176
|
+
source: resolved.source,
|
|
177
|
+
toolFilter: options.tool,
|
|
178
|
+
invalidLines,
|
|
179
|
+
summary,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (options.json) {
|
|
183
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
printHuman(report);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
main();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(error.message);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|