token-pilot 0.13.0 → 0.14.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/.claude-plugin/hooks/hooks.json +9 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +29 -0
- package/README.md +36 -15
- package/dist/config/defaults.js +12 -0
- package/dist/core/architecture-fingerprint.d.ts +34 -0
- package/dist/core/architecture-fingerprint.js +127 -0
- package/dist/core/budget-planner.d.ts +21 -0
- package/dist/core/budget-planner.js +68 -0
- package/dist/core/confidence.d.ts +31 -0
- package/dist/core/confidence.js +99 -0
- package/dist/core/context-registry.d.ts +14 -0
- package/dist/core/context-registry.js +55 -0
- package/dist/core/decision-trace.d.ts +31 -0
- package/dist/core/decision-trace.js +45 -0
- package/dist/core/intent-classifier.d.ts +13 -0
- package/dist/core/intent-classifier.js +44 -0
- package/dist/core/policy-engine.d.ts +41 -0
- package/dist/core/policy-engine.js +76 -0
- package/dist/core/session-analytics.d.ts +8 -0
- package/dist/core/session-analytics.js +86 -7
- package/dist/core/session-cache.d.ts +74 -0
- package/dist/core/session-cache.js +162 -0
- package/dist/core/validation.d.ts +3 -0
- package/dist/core/validation.js +3 -0
- package/dist/git/file-watcher.d.ts +6 -0
- package/dist/git/file-watcher.js +18 -2
- package/dist/git/watcher.d.ts +3 -0
- package/dist/git/watcher.js +6 -0
- package/dist/handlers/code-audit.d.ts +7 -2
- package/dist/handlers/code-audit.js +19 -5
- package/dist/handlers/explore-area.d.ts +10 -0
- package/dist/handlers/explore-area.js +39 -13
- package/dist/handlers/find-unused.d.ts +3 -0
- package/dist/handlers/find-unused.js +3 -2
- package/dist/handlers/find-usages.d.ts +7 -0
- package/dist/handlers/find-usages.js +36 -5
- package/dist/handlers/module-info.d.ts +3 -0
- package/dist/handlers/module-info.js +22 -2
- package/dist/handlers/project-overview.d.ts +1 -1
- package/dist/handlers/project-overview.js +18 -2
- package/dist/handlers/read-for-edit.d.ts +3 -0
- package/dist/handlers/read-for-edit.js +185 -3
- package/dist/handlers/read-range.d.ts +1 -1
- package/dist/handlers/read-range.js +16 -1
- package/dist/handlers/read-symbol.d.ts +1 -1
- package/dist/handlers/read-symbol.js +26 -2
- package/dist/handlers/related-files.d.ts +11 -0
- package/dist/handlers/related-files.js +178 -42
- package/dist/handlers/smart-read-many.js +70 -16
- package/dist/handlers/smart-read.js +10 -1
- package/dist/handlers/test-summary.js +26 -3
- package/dist/hooks/installer.d.ts +12 -8
- package/dist/hooks/installer.js +24 -8
- package/dist/index.d.ts +16 -1
- package/dist/index.js +62 -56
- package/dist/server.js +395 -30
- package/dist/types.d.ts +12 -0
- package/package.json +18 -14
- package/start.sh +28 -27
- package/dist/handlers/class-hierarchy.d.ts +0 -11
- package/dist/handlers/class-hierarchy.js +0 -28
- package/dist/handlers/export-ast-index.d.ts +0 -22
- package/dist/handlers/export-ast-index.js +0 -175
- package/dist/handlers/find-implementations.d.ts +0 -11
- package/dist/handlers/find-implementations.js +0 -27
- package/dist/handlers/search-code.d.ts +0 -14
- package/dist/handlers/search-code.js +0 -32
package/dist/server.js
CHANGED
|
@@ -5,9 +5,12 @@ import { FileCache } from './core/file-cache.js';
|
|
|
5
5
|
import { ContextRegistry } from './core/context-registry.js';
|
|
6
6
|
import { SymbolResolver } from './core/symbol-resolver.js';
|
|
7
7
|
import { SessionAnalytics } from './core/session-analytics.js';
|
|
8
|
+
import { classifyIntent } from './core/intent-classifier.js';
|
|
9
|
+
import { buildDecisionTrace } from './core/decision-trace.js';
|
|
10
|
+
import { SessionCache } from './core/session-cache.js';
|
|
8
11
|
import { loadConfig } from './config/loader.js';
|
|
9
12
|
import { readFileSync } from 'node:fs';
|
|
10
|
-
import { dirname } from 'node:path';
|
|
13
|
+
import { dirname, resolve } from 'node:path';
|
|
11
14
|
import { execFile } from 'node:child_process';
|
|
12
15
|
import { isDangerousRoot } from './core/validation.js';
|
|
13
16
|
import { promisify } from 'node:util';
|
|
@@ -25,7 +28,7 @@ import { handleNonCodeRead, isNonCodeStructured } from './handlers/non-code.js';
|
|
|
25
28
|
import { handleFindUnused } from './handlers/find-unused.js';
|
|
26
29
|
import { handleReadForEdit } from './handlers/read-for-edit.js';
|
|
27
30
|
import { handleRelatedFiles } from './handlers/related-files.js';
|
|
28
|
-
import { handleOutline } from './handlers/outline.js';
|
|
31
|
+
import { handleOutline, CODE_EXTENSIONS } from './handlers/outline.js';
|
|
29
32
|
import { handleCodeAudit } from './handlers/code-audit.js';
|
|
30
33
|
import { handleModuleInfo } from './handlers/module-info.js';
|
|
31
34
|
import { handleSmartDiff } from './handlers/smart-diff.js';
|
|
@@ -34,6 +37,7 @@ import { handleSmartLog } from './handlers/smart-log.js';
|
|
|
34
37
|
import { handleTestSummary } from './handlers/test-summary.js';
|
|
35
38
|
import { detectContextMode } from './integration/context-mode-detector.js';
|
|
36
39
|
import { estimateTokens } from './core/token-estimator.js';
|
|
40
|
+
import { checkPolicy, isFullReadTool } from './core/policy-engine.js';
|
|
37
41
|
import { resolveSafePath, validateSmartReadArgs, validateReadSymbolArgs, validateReadRangeArgs, validateReadDiffArgs, validateFindUsagesArgs, validateSmartReadManyArgs, validateReadForEditArgs, validateRelatedFilesArgs, validateOutlineArgs, validateFindUnusedArgs, validateCodeAuditArgs, validateProjectOverviewArgs, validateModuleInfoArgs, validateSmartDiffArgs, validateExploreAreaArgs, validateSmartLogArgs, validateTestSummaryArgs, } from './core/validation.js';
|
|
38
42
|
export async function createServer(projectRoot, options) {
|
|
39
43
|
const config = await loadConfig(projectRoot);
|
|
@@ -139,6 +143,13 @@ export async function createServer(projectRoot, options) {
|
|
|
139
143
|
}
|
|
140
144
|
// Session analytics
|
|
141
145
|
const analytics = new SessionAnalytics();
|
|
146
|
+
// Session cache (tool-result-level caching, invalidated by file/AST/git changes)
|
|
147
|
+
const sessionCache = config.sessionCache.enabled
|
|
148
|
+
? new SessionCache(config.sessionCache.maxEntries)
|
|
149
|
+
: null;
|
|
150
|
+
// Policy engine state
|
|
151
|
+
let fullFileReadsCount = 0;
|
|
152
|
+
const readForEditCalled = new Set();
|
|
142
153
|
// Detect context-mode companion
|
|
143
154
|
const cmEnabled = config.contextMode.enabled;
|
|
144
155
|
const contextModeStatus = await detectContextMode(projectRoot, cmEnabled === 'auto' ? undefined : cmEnabled);
|
|
@@ -161,6 +172,17 @@ export async function createServer(projectRoot, options) {
|
|
|
161
172
|
fileWatcher = new FileWatcher(projectRoot, fileCache, contextRegistry, config.ignore, astIndex);
|
|
162
173
|
fileWatcher.start();
|
|
163
174
|
fileCache.onSet((filePath) => fileWatcher?.watchFile(filePath));
|
|
175
|
+
if (sessionCache) {
|
|
176
|
+
fileWatcher.onFileChange((absPath) => sessionCache.invalidateByFiles([absPath]));
|
|
177
|
+
fileWatcher.onAstUpdate(() => sessionCache.invalidateByAst());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Wire session cache to git watcher
|
|
181
|
+
if (sessionCache) {
|
|
182
|
+
gitWatcher.onBranchSwitchEvent((changedFiles) => {
|
|
183
|
+
sessionCache.invalidateByFiles(changedFiles);
|
|
184
|
+
sessionCache.invalidateByGit();
|
|
185
|
+
});
|
|
164
186
|
}
|
|
165
187
|
// Read version from package.json
|
|
166
188
|
let pkgVersion = '0.1.1';
|
|
@@ -271,7 +293,7 @@ export async function createServer(projectRoot, options) {
|
|
|
271
293
|
},
|
|
272
294
|
{
|
|
273
295
|
name: 'read_for_edit',
|
|
274
|
-
description: 'Use INSTEAD OF Read when preparing an edit. Returns exact raw code around a symbol or line — copy directly as old_string for Edit tool.',
|
|
296
|
+
description: 'Use INSTEAD OF Read when preparing an edit. Returns exact raw code around a symbol or line — copy directly as old_string for Edit tool. Optional: include_callers, include_tests, include_changes for enriched context.',
|
|
275
297
|
inputSchema: {
|
|
276
298
|
type: 'object',
|
|
277
299
|
properties: {
|
|
@@ -279,6 +301,9 @@ export async function createServer(projectRoot, options) {
|
|
|
279
301
|
symbol: { type: 'string', description: 'Symbol name to edit (e.g. "UserService.updateUser")' },
|
|
280
302
|
line: { type: 'number', description: 'Line number to edit (alternative to symbol)' },
|
|
281
303
|
context: { type: 'number', description: 'Lines of context around target (default: 5)' },
|
|
304
|
+
include_callers: { type: 'boolean', description: 'Show top callers of this symbol (saves a separate find_usages call)' },
|
|
305
|
+
include_tests: { type: 'boolean', description: 'Show related test file and test names' },
|
|
306
|
+
include_changes: { type: 'boolean', description: 'Show recent git changes in the target region' },
|
|
282
307
|
},
|
|
283
308
|
required: ['path'],
|
|
284
309
|
},
|
|
@@ -330,7 +355,7 @@ export async function createServer(projectRoot, options) {
|
|
|
330
355
|
},
|
|
331
356
|
{
|
|
332
357
|
name: 'related_files',
|
|
333
|
-
description: 'Show import graph for a file:
|
|
358
|
+
description: 'Show ranked import graph for a file: imports, importers, and tests scored by relevance (test adjacency, import closeness, recent changes, path proximity). Files ranked into HIGH VALUE / MEDIUM / LOW to prioritize reading.',
|
|
334
359
|
inputSchema: {
|
|
335
360
|
type: 'object',
|
|
336
361
|
properties: {
|
|
@@ -480,6 +505,205 @@ export async function createServer(projectRoot, options) {
|
|
|
480
505
|
return 0;
|
|
481
506
|
}
|
|
482
507
|
}
|
|
508
|
+
async function estimateProjectOverviewWorkflowTokens(includeSections) {
|
|
509
|
+
const sectionFiles = {
|
|
510
|
+
stack: ['package.json', 'composer.json', 'Cargo.toml', 'pyproject.toml', 'go.mod'],
|
|
511
|
+
ci: ['.gitlab-ci.yml', 'Jenkinsfile', '.circleci/config.yml', 'bitbucket-pipelines.yml', '.travis.yml'],
|
|
512
|
+
quality: [
|
|
513
|
+
'tsconfig.json',
|
|
514
|
+
'vitest.config.ts',
|
|
515
|
+
'vitest.config.js',
|
|
516
|
+
'vitest.config.mts',
|
|
517
|
+
'jest.config.js',
|
|
518
|
+
'jest.config.ts',
|
|
519
|
+
'jest.config.mjs',
|
|
520
|
+
'eslint.config.js',
|
|
521
|
+
'eslint.config.mjs',
|
|
522
|
+
'.eslintrc',
|
|
523
|
+
'.eslintrc.js',
|
|
524
|
+
'.eslintrc.json',
|
|
525
|
+
'.eslintrc.yml',
|
|
526
|
+
'biome.json',
|
|
527
|
+
'biome.jsonc',
|
|
528
|
+
'.prettierrc',
|
|
529
|
+
'.prettierrc.js',
|
|
530
|
+
'.prettierrc.json',
|
|
531
|
+
'prettier.config.js',
|
|
532
|
+
'phpunit.xml',
|
|
533
|
+
'phpunit.xml.dist',
|
|
534
|
+
'phpstan.neon',
|
|
535
|
+
'phpstan.neon.dist',
|
|
536
|
+
],
|
|
537
|
+
architecture: ['README.md'],
|
|
538
|
+
};
|
|
539
|
+
let total = 0;
|
|
540
|
+
const seen = new Set();
|
|
541
|
+
for (const section of includeSections) {
|
|
542
|
+
for (const file of sectionFiles[section]) {
|
|
543
|
+
if (seen.has(file))
|
|
544
|
+
continue;
|
|
545
|
+
seen.add(file);
|
|
546
|
+
total += await fullFileTokens(file);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (includeSections.includes('ci')) {
|
|
550
|
+
try {
|
|
551
|
+
const { readdir: readDirAsync } = await import('node:fs/promises');
|
|
552
|
+
const workflowDir = resolveSafePath(projectRoot, '.github/workflows');
|
|
553
|
+
const workflowFiles = await readDirAsync(workflowDir, { withFileTypes: true });
|
|
554
|
+
for (const file of workflowFiles) {
|
|
555
|
+
if (!file.isFile())
|
|
556
|
+
continue;
|
|
557
|
+
if (!file.name.endsWith('.yml') && !file.name.endsWith('.yaml'))
|
|
558
|
+
continue;
|
|
559
|
+
total += await fullFileTokens(`.github/workflows/${file.name}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// ignore missing workflows dir
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (includeSections.includes('architecture')) {
|
|
567
|
+
total += 200;
|
|
568
|
+
}
|
|
569
|
+
return total;
|
|
570
|
+
}
|
|
571
|
+
async function estimateOutlineWorkflowTokens(relativePath, recursive, maxDepth) {
|
|
572
|
+
const SAMPLE_LIMIT = 30;
|
|
573
|
+
try {
|
|
574
|
+
const { readdir: readDirAsync } = await import('node:fs/promises');
|
|
575
|
+
const { resolve: resolvePath } = await import('node:path');
|
|
576
|
+
const absDir = resolveSafePath(projectRoot, relativePath);
|
|
577
|
+
const sampledFiles = [];
|
|
578
|
+
let totalFiles = 0;
|
|
579
|
+
async function walk(dirPath, depth) {
|
|
580
|
+
const entries = await readDirAsync(dirPath, { withFileTypes: true });
|
|
581
|
+
for (const entry of entries) {
|
|
582
|
+
if (entry.isFile()) {
|
|
583
|
+
const ext = entry.name.split('.').pop()?.toLowerCase() ?? '';
|
|
584
|
+
if (!CODE_EXTENSIONS.has(ext))
|
|
585
|
+
continue;
|
|
586
|
+
totalFiles++;
|
|
587
|
+
if (sampledFiles.length < SAMPLE_LIMIT) {
|
|
588
|
+
sampledFiles.push(resolvePath(dirPath, entry.name));
|
|
589
|
+
}
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (entry.isDirectory() && recursive && depth < maxDepth) {
|
|
593
|
+
await walk(resolvePath(dirPath, entry.name), depth + 1);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
await walk(absDir, 0);
|
|
598
|
+
if (totalFiles === 0)
|
|
599
|
+
return 0;
|
|
600
|
+
let sampledTokens = 0;
|
|
601
|
+
for (const filePath of sampledFiles) {
|
|
602
|
+
const relPath = filePath.startsWith(projectRoot)
|
|
603
|
+
? filePath.slice(projectRoot.length + 1)
|
|
604
|
+
: filePath;
|
|
605
|
+
sampledTokens += await fullFileTokens(relPath);
|
|
606
|
+
}
|
|
607
|
+
if (sampledFiles.length === 0 || sampledTokens === 0)
|
|
608
|
+
return 0;
|
|
609
|
+
if (sampledFiles.length === totalFiles)
|
|
610
|
+
return sampledTokens;
|
|
611
|
+
const averageTokens = sampledTokens / sampledFiles.length;
|
|
612
|
+
return Math.round(averageTokens * totalFiles);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
return 0;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function estimateRelatedFilesWorkflowTokens(targetPath, meta) {
|
|
619
|
+
const related = new Set([targetPath]);
|
|
620
|
+
for (const path of meta?.imports ?? [])
|
|
621
|
+
related.add(path);
|
|
622
|
+
for (const path of meta?.importedBy ?? [])
|
|
623
|
+
related.add(path);
|
|
624
|
+
for (const path of meta?.tests ?? [])
|
|
625
|
+
related.add(path);
|
|
626
|
+
let total = 0;
|
|
627
|
+
let counted = 0;
|
|
628
|
+
for (const path of related) {
|
|
629
|
+
total += await fullFileTokens(path);
|
|
630
|
+
counted++;
|
|
631
|
+
if (counted >= 12)
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
return total;
|
|
635
|
+
}
|
|
636
|
+
async function estimateFindUsagesWorkflowTokens(files) {
|
|
637
|
+
let total = 0;
|
|
638
|
+
let counted = 0;
|
|
639
|
+
for (const file of files) {
|
|
640
|
+
total += await fullFileTokens(file);
|
|
641
|
+
counted++;
|
|
642
|
+
if (counted >= 20)
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
return total;
|
|
646
|
+
}
|
|
647
|
+
/** Detect savings category from response text prefix. */
|
|
648
|
+
function detectSavingsCategory(text) {
|
|
649
|
+
if (text.startsWith('REMINDER:') || text.startsWith('DEDUP:'))
|
|
650
|
+
return 'dedup';
|
|
651
|
+
return 'compression';
|
|
652
|
+
}
|
|
653
|
+
/** Record analytics with intent classification and decision trace. Returns policy advisory if any. */
|
|
654
|
+
function recordWithTrace(call) {
|
|
655
|
+
const { absPath, args, recentlyEdited, ...rest } = call;
|
|
656
|
+
analytics.record({
|
|
657
|
+
...rest,
|
|
658
|
+
intent: classifyIntent(rest.tool),
|
|
659
|
+
decisionTrace: buildDecisionTrace({
|
|
660
|
+
absPath,
|
|
661
|
+
tool: rest.tool,
|
|
662
|
+
args: (args ?? {}),
|
|
663
|
+
contextRegistry,
|
|
664
|
+
fileCache,
|
|
665
|
+
tokensReturned: rest.tokensReturned,
|
|
666
|
+
tokensWouldBe: rest.tokensWouldBe,
|
|
667
|
+
recentlyEdited,
|
|
668
|
+
}),
|
|
669
|
+
});
|
|
670
|
+
// Policy tracking
|
|
671
|
+
if (isFullReadTool(rest.tool)) {
|
|
672
|
+
fullFileReadsCount++;
|
|
673
|
+
}
|
|
674
|
+
if (rest.tool === 'read_for_edit' && call.path) {
|
|
675
|
+
readForEditCalled.add(call.path);
|
|
676
|
+
}
|
|
677
|
+
// Policy check
|
|
678
|
+
const advisory = checkPolicy(config.policies, rest.tool, {
|
|
679
|
+
fullFileReadsCount,
|
|
680
|
+
tokensReturned: rest.tokensReturned,
|
|
681
|
+
readForEditCalled,
|
|
682
|
+
});
|
|
683
|
+
return advisory ? `\n${advisory.message}` : null;
|
|
684
|
+
}
|
|
685
|
+
async function estimateExploreAreaWorkflowTokens(meta) {
|
|
686
|
+
const localFiles = new Set();
|
|
687
|
+
for (const file of meta.codeFiles ?? [])
|
|
688
|
+
localFiles.add(file);
|
|
689
|
+
for (const file of meta.testFiles ?? [])
|
|
690
|
+
localFiles.add(file);
|
|
691
|
+
for (const file of meta.internalDeps ?? [])
|
|
692
|
+
localFiles.add(file);
|
|
693
|
+
for (const file of meta.importedBy ?? [])
|
|
694
|
+
localFiles.add(file);
|
|
695
|
+
let total = 0;
|
|
696
|
+
let counted = 0;
|
|
697
|
+
for (const file of localFiles) {
|
|
698
|
+
total += await fullFileTokens(file);
|
|
699
|
+
counted++;
|
|
700
|
+
if (counted >= 24)
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
total += (meta.externalDeps?.length ?? 0) * 30;
|
|
704
|
+
total += (meta.changeCount ?? 0) * 40;
|
|
705
|
+
return total;
|
|
706
|
+
}
|
|
483
707
|
// Handle tool calls with validated arguments
|
|
484
708
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
485
709
|
const { name, arguments: args } = request.params;
|
|
@@ -502,32 +726,44 @@ export async function createServer(projectRoot, options) {
|
|
|
502
726
|
});
|
|
503
727
|
if (nonCodeResult) {
|
|
504
728
|
const text = nonCodeResult.content[0]?.text ?? '';
|
|
505
|
-
|
|
729
|
+
recordWithTrace({
|
|
730
|
+
tool: 'smart_read',
|
|
731
|
+
path: validArgs.path,
|
|
732
|
+
tokensReturned: estimateTokens(text),
|
|
733
|
+
tokensWouldBe: await fullFileTokens(validArgs.path) || estimateTokens(text),
|
|
734
|
+
timestamp: Date.now(),
|
|
735
|
+
delegatedToContextMode: text.includes('ADVISORY:') && text.includes('context-mode'),
|
|
736
|
+
savingsCategory: 'compression',
|
|
737
|
+
absPath: resolve(projectRoot, validArgs.path),
|
|
738
|
+
args: validArgs,
|
|
739
|
+
});
|
|
506
740
|
return nonCodeResult;
|
|
507
741
|
}
|
|
508
742
|
}
|
|
509
743
|
const result = await handleSmartRead(validArgs, projectRoot, astIndex, fileCache, contextRegistry, config);
|
|
510
744
|
const text = result.content[0]?.text ?? '';
|
|
511
745
|
const fullTokensSR = await fullFileTokens(validArgs.path);
|
|
512
|
-
|
|
746
|
+
const policyAdv = recordWithTrace({ tool: 'smart_read', path: validArgs.path, tokensReturned: estimateTokens(text), tokensWouldBe: fullTokensSR || estimateTokens(text), timestamp: Date.now(), savingsCategory: detectSavingsCategory(text), absPath: resolve(projectRoot, validArgs.path), args: validArgs });
|
|
747
|
+
if (policyAdv)
|
|
748
|
+
result.content[0] = { type: 'text', text: text + policyAdv };
|
|
513
749
|
return result;
|
|
514
750
|
}
|
|
515
751
|
case 'read_symbol': {
|
|
516
752
|
const symArgs = validateReadSymbolArgs(args);
|
|
517
|
-
const symResult = await handleReadSymbol(symArgs, projectRoot, symbolResolver, fileCache, contextRegistry, astIndex);
|
|
753
|
+
const symResult = await handleReadSymbol(symArgs, projectRoot, symbolResolver, fileCache, contextRegistry, astIndex, config.smartRead.advisoryReminders);
|
|
518
754
|
const symText = symResult.content[0]?.text ?? '';
|
|
519
755
|
const symTokens = estimateTokens(symText);
|
|
520
756
|
const fullTokensSym = await fullFileTokens(symArgs.path);
|
|
521
|
-
|
|
757
|
+
recordWithTrace({ tool: 'read_symbol', path: symArgs.path, tokensReturned: symTokens, tokensWouldBe: fullTokensSym || symTokens, timestamp: Date.now(), savingsCategory: detectSavingsCategory(symText), absPath: resolve(projectRoot, symArgs.path), args: symArgs });
|
|
522
758
|
return symResult;
|
|
523
759
|
}
|
|
524
760
|
case 'read_range': {
|
|
525
761
|
const rangeArgs = validateReadRangeArgs(args);
|
|
526
|
-
const rangeResult = await handleReadRange(rangeArgs, projectRoot, fileCache, contextRegistry);
|
|
762
|
+
const rangeResult = await handleReadRange(rangeArgs, projectRoot, fileCache, contextRegistry, config.smartRead.advisoryReminders);
|
|
527
763
|
const rangeText = rangeResult.content[0]?.text ?? '';
|
|
528
764
|
const rangeTokens = estimateTokens(rangeText);
|
|
529
765
|
const fullTokensRange = await fullFileTokens(rangeArgs.path);
|
|
530
|
-
|
|
766
|
+
recordWithTrace({ tool: 'read_range', path: rangeArgs.path, tokensReturned: rangeTokens, tokensWouldBe: fullTokensRange || rangeTokens, timestamp: Date.now(), savingsCategory: detectSavingsCategory(rangeText), absPath: resolve(projectRoot, rangeArgs.path), args: rangeArgs });
|
|
531
767
|
return rangeResult;
|
|
532
768
|
}
|
|
533
769
|
case 'read_diff': {
|
|
@@ -536,7 +772,7 @@ export async function createServer(projectRoot, options) {
|
|
|
536
772
|
const diffText = diffResult.content[0]?.text ?? '';
|
|
537
773
|
const diffTokens = estimateTokens(diffText);
|
|
538
774
|
const fullTokensDiff = await fullFileTokens(diffArgs.path);
|
|
539
|
-
|
|
775
|
+
recordWithTrace({ tool: 'read_diff', path: diffArgs.path, tokensReturned: diffTokens, tokensWouldBe: fullTokensDiff || diffTokens, timestamp: Date.now(), savingsCategory: 'compression', absPath: resolve(projectRoot, diffArgs.path), args: diffArgs });
|
|
540
776
|
return diffResult;
|
|
541
777
|
}
|
|
542
778
|
case 'read_for_edit': {
|
|
@@ -545,7 +781,7 @@ export async function createServer(projectRoot, options) {
|
|
|
545
781
|
const editText = editResult.content[0]?.text ?? '';
|
|
546
782
|
const editTokens = estimateTokens(editText);
|
|
547
783
|
const fullTokensEdit = await fullFileTokens(editArgs.path);
|
|
548
|
-
|
|
784
|
+
recordWithTrace({ tool: 'read_for_edit', path: editArgs.path, tokensReturned: editTokens, tokensWouldBe: fullTokensEdit || editTokens, timestamp: Date.now(), savingsCategory: 'compression', absPath: resolve(projectRoot, editArgs.path), args: editArgs });
|
|
549
785
|
return editResult;
|
|
550
786
|
}
|
|
551
787
|
case 'smart_read_many': {
|
|
@@ -553,66 +789,178 @@ export async function createServer(projectRoot, options) {
|
|
|
553
789
|
const manyResult = await handleSmartReadMany(manyArgs, projectRoot, astIndex, fileCache, contextRegistry, config);
|
|
554
790
|
const manyText = manyResult.content[0]?.text ?? '';
|
|
555
791
|
const manyTokens = estimateTokens(manyText);
|
|
792
|
+
const uniqueManyPaths = Array.from(new Set(manyArgs.paths));
|
|
556
793
|
let fullTokensMany = 0;
|
|
557
|
-
for (const p of
|
|
794
|
+
for (const p of uniqueManyPaths) {
|
|
558
795
|
fullTokensMany += await fullFileTokens(p);
|
|
559
796
|
}
|
|
560
|
-
|
|
797
|
+
recordWithTrace({
|
|
798
|
+
tool: 'smart_read_many',
|
|
799
|
+
path: uniqueManyPaths.join(', '),
|
|
800
|
+
tokensReturned: manyTokens,
|
|
801
|
+
tokensWouldBe: fullTokensMany || manyTokens,
|
|
802
|
+
timestamp: Date.now(),
|
|
803
|
+
savingsCategory: 'compression',
|
|
804
|
+
args: manyArgs,
|
|
805
|
+
});
|
|
561
806
|
return manyResult;
|
|
562
807
|
}
|
|
563
808
|
case 'find_usages': {
|
|
564
809
|
const usagesArgs = validateFindUsagesArgs(args);
|
|
810
|
+
const cachedUsages = sessionCache?.get('find_usages', usagesArgs);
|
|
811
|
+
if (cachedUsages) {
|
|
812
|
+
recordWithTrace({ tool: 'find_usages', path: usagesArgs.symbol, tokensReturned: cachedUsages.tokenEstimate, tokensWouldBe: cachedUsages.tokensWouldBe ?? cachedUsages.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: usagesArgs });
|
|
813
|
+
return cachedUsages.result;
|
|
814
|
+
}
|
|
565
815
|
const usagesResult = await handleFindUsages(usagesArgs, astIndex);
|
|
566
816
|
const usagesText = usagesResult.content[0]?.text ?? '';
|
|
567
|
-
|
|
817
|
+
const usagesTokens = estimateTokens(usagesText);
|
|
818
|
+
const usagesWouldBe = await estimateFindUsagesWorkflowTokens(usagesResult.meta.files);
|
|
819
|
+
sessionCache?.set('find_usages', usagesArgs, usagesResult, {
|
|
820
|
+
files: usagesResult.meta.files.map(f => resolve(projectRoot, f)),
|
|
821
|
+
dependsOnAst: true,
|
|
822
|
+
}, usagesTokens, usagesWouldBe || usagesTokens);
|
|
823
|
+
recordWithTrace({
|
|
824
|
+
tool: 'find_usages',
|
|
825
|
+
path: usagesArgs.symbol,
|
|
826
|
+
tokensReturned: usagesTokens,
|
|
827
|
+
tokensWouldBe: usagesWouldBe || usagesTokens,
|
|
828
|
+
timestamp: Date.now(),
|
|
829
|
+
savingsCategory: 'compression',
|
|
830
|
+
args: usagesArgs,
|
|
831
|
+
});
|
|
568
832
|
return usagesResult;
|
|
569
833
|
}
|
|
570
834
|
case 'project_overview': {
|
|
571
835
|
const overviewArgs = validateProjectOverviewArgs(args);
|
|
572
|
-
const
|
|
836
|
+
const cachedOverview = sessionCache?.get('project_overview', overviewArgs);
|
|
837
|
+
if (cachedOverview) {
|
|
838
|
+
recordWithTrace({ tool: 'project_overview', path: projectRoot, tokensReturned: cachedOverview.tokenEstimate, tokensWouldBe: cachedOverview.tokensWouldBe ?? cachedOverview.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: overviewArgs });
|
|
839
|
+
return cachedOverview.result;
|
|
840
|
+
}
|
|
841
|
+
const overviewResult = await handleProjectOverview(overviewArgs, projectRoot, astIndex, pkgVersion);
|
|
573
842
|
const overviewText = overviewResult.content[0]?.text ?? '';
|
|
574
843
|
overviewResult.content[0] = { type: 'text', text: `TOKEN PILOT v${pkgVersion}\n\n${overviewText}` };
|
|
575
844
|
const ovTokens = estimateTokens(overviewResult.content[0].text);
|
|
576
|
-
|
|
845
|
+
const overviewWouldBe = await estimateProjectOverviewWorkflowTokens(overviewArgs.include ?? ['stack', 'ci', 'quality', 'architecture']);
|
|
846
|
+
sessionCache?.set('project_overview', overviewArgs, overviewResult, {
|
|
847
|
+
dependsOnAst: true,
|
|
848
|
+
}, ovTokens, overviewWouldBe || ovTokens);
|
|
849
|
+
recordWithTrace({
|
|
850
|
+
tool: 'project_overview',
|
|
851
|
+
path: projectRoot,
|
|
852
|
+
tokensReturned: ovTokens,
|
|
853
|
+
tokensWouldBe: overviewWouldBe || ovTokens,
|
|
854
|
+
timestamp: Date.now(),
|
|
855
|
+
savingsCategory: 'compression',
|
|
856
|
+
args: overviewArgs,
|
|
857
|
+
});
|
|
577
858
|
return overviewResult;
|
|
578
859
|
}
|
|
579
860
|
case 'related_files': {
|
|
580
861
|
const relArgs = validateRelatedFilesArgs(args);
|
|
862
|
+
const cachedRel = sessionCache?.get('related_files', relArgs);
|
|
863
|
+
if (cachedRel) {
|
|
864
|
+
recordWithTrace({ tool: 'related_files', path: relArgs.path, tokensReturned: cachedRel.tokenEstimate, tokensWouldBe: cachedRel.tokensWouldBe ?? cachedRel.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', absPath: resolve(projectRoot, relArgs.path), args: relArgs });
|
|
865
|
+
return cachedRel.result;
|
|
866
|
+
}
|
|
581
867
|
const relResult = await handleRelatedFiles(relArgs, projectRoot, astIndex);
|
|
582
868
|
const relText = relResult.content[0]?.text ?? '';
|
|
583
|
-
|
|
869
|
+
const relTokens = estimateTokens(relText);
|
|
870
|
+
const relWouldBe = await estimateRelatedFilesWorkflowTokens(relArgs.path, relResult.meta);
|
|
871
|
+
const relDeps = [
|
|
872
|
+
resolve(projectRoot, relArgs.path),
|
|
873
|
+
...relResult.meta.imports.map(f => resolve(projectRoot, f)),
|
|
874
|
+
...relResult.meta.importedBy.map(f => resolve(projectRoot, f)),
|
|
875
|
+
...relResult.meta.tests.map(f => resolve(projectRoot, f)),
|
|
876
|
+
];
|
|
877
|
+
sessionCache?.set('related_files', relArgs, relResult, {
|
|
878
|
+
files: relDeps,
|
|
879
|
+
dependsOnAst: true,
|
|
880
|
+
}, relTokens, relWouldBe || relTokens);
|
|
881
|
+
recordWithTrace({
|
|
882
|
+
tool: 'related_files',
|
|
883
|
+
path: relArgs.path,
|
|
884
|
+
tokensReturned: relTokens,
|
|
885
|
+
tokensWouldBe: relWouldBe || relTokens,
|
|
886
|
+
timestamp: Date.now(),
|
|
887
|
+
savingsCategory: 'compression',
|
|
888
|
+
absPath: resolve(projectRoot, relArgs.path),
|
|
889
|
+
args: relArgs,
|
|
890
|
+
});
|
|
584
891
|
return relResult;
|
|
585
892
|
}
|
|
586
893
|
case 'outline': {
|
|
587
894
|
const outlineArgs = validateOutlineArgs(args);
|
|
895
|
+
const cachedOutline = sessionCache?.get('outline', outlineArgs);
|
|
896
|
+
if (cachedOutline) {
|
|
897
|
+
recordWithTrace({ tool: 'outline', path: outlineArgs.path, tokensReturned: cachedOutline.tokenEstimate, tokensWouldBe: cachedOutline.tokensWouldBe ?? cachedOutline.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: outlineArgs });
|
|
898
|
+
return cachedOutline.result;
|
|
899
|
+
}
|
|
588
900
|
const outlineResult = await handleOutline(outlineArgs, projectRoot, astIndex);
|
|
589
901
|
const outlineText = outlineResult.content[0]?.text ?? '';
|
|
590
|
-
|
|
902
|
+
const outlineTokens = estimateTokens(outlineText);
|
|
903
|
+
const outlineWouldBe = await estimateOutlineWorkflowTokens(outlineArgs.path, outlineArgs.recursive ?? false, outlineArgs.max_depth ?? 2);
|
|
904
|
+
sessionCache?.set('outline', outlineArgs, outlineResult, {
|
|
905
|
+
files: [resolve(projectRoot, outlineArgs.path) + '/'],
|
|
906
|
+
dependsOnAst: true,
|
|
907
|
+
}, outlineTokens, outlineWouldBe || outlineTokens);
|
|
908
|
+
recordWithTrace({
|
|
909
|
+
tool: 'outline',
|
|
910
|
+
path: outlineArgs.path,
|
|
911
|
+
tokensReturned: outlineTokens,
|
|
912
|
+
tokensWouldBe: outlineWouldBe || outlineTokens,
|
|
913
|
+
timestamp: Date.now(),
|
|
914
|
+
savingsCategory: 'compression',
|
|
915
|
+
args: outlineArgs,
|
|
916
|
+
});
|
|
591
917
|
return outlineResult;
|
|
592
918
|
}
|
|
593
919
|
case 'session_analytics':
|
|
594
920
|
return { content: [{ type: 'text', text: `TOKEN PILOT v${pkgVersion}\n\n${analytics.report()}` }] };
|
|
595
921
|
case 'find_unused': {
|
|
596
922
|
const unusedArgs = validateFindUnusedArgs(args);
|
|
923
|
+
const cachedUnused = sessionCache?.get('find_unused', unusedArgs);
|
|
924
|
+
if (cachedUnused) {
|
|
925
|
+
recordWithTrace({ tool: 'find_unused', path: unusedArgs.module ?? 'all', tokensReturned: cachedUnused.tokenEstimate, tokensWouldBe: cachedUnused.tokensWouldBe ?? cachedUnused.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: unusedArgs });
|
|
926
|
+
return cachedUnused.result;
|
|
927
|
+
}
|
|
597
928
|
const unusedResult = await handleFindUnused(unusedArgs, astIndex);
|
|
598
929
|
const unusedText = unusedResult.content[0]?.text ?? '';
|
|
599
|
-
|
|
930
|
+
const unusedTokens = estimateTokens(unusedText);
|
|
931
|
+
const unusedWouldBe = await estimateFindUsagesWorkflowTokens(unusedResult.meta.files);
|
|
932
|
+
sessionCache?.set('find_unused', unusedArgs, unusedResult, { dependsOnAst: true }, unusedTokens, unusedWouldBe || unusedTokens);
|
|
933
|
+
recordWithTrace({ tool: 'find_unused', path: unusedArgs.module ?? 'all', tokensReturned: unusedTokens, tokensWouldBe: unusedWouldBe || unusedTokens, timestamp: Date.now(), savingsCategory: 'compression', args: unusedArgs });
|
|
600
934
|
return unusedResult;
|
|
601
935
|
}
|
|
602
936
|
case 'code_audit': {
|
|
603
937
|
const auditArgs = validateCodeAuditArgs(args);
|
|
938
|
+
const cachedAudit = sessionCache?.get('code_audit', auditArgs);
|
|
939
|
+
if (cachedAudit) {
|
|
940
|
+
recordWithTrace({ tool: 'code_audit', path: auditArgs.check, tokensReturned: cachedAudit.tokenEstimate, tokensWouldBe: cachedAudit.tokensWouldBe ?? cachedAudit.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: auditArgs });
|
|
941
|
+
return cachedAudit.result;
|
|
942
|
+
}
|
|
604
943
|
const auditResult = await handleCodeAudit(auditArgs, projectRoot, astIndex);
|
|
605
944
|
const auditText = auditResult.content[0]?.text ?? '';
|
|
606
|
-
|
|
945
|
+
const auditTokens = estimateTokens(auditText);
|
|
946
|
+
const auditWouldBe = await estimateFindUsagesWorkflowTokens(auditResult.meta.files);
|
|
947
|
+
sessionCache?.set('code_audit', auditArgs, auditResult, { dependsOnAst: true }, auditTokens, auditWouldBe || auditTokens);
|
|
948
|
+
recordWithTrace({ tool: 'code_audit', path: auditArgs.check, tokensReturned: auditTokens, tokensWouldBe: auditWouldBe || auditTokens, timestamp: Date.now(), savingsCategory: 'compression', args: auditArgs });
|
|
607
949
|
return auditResult;
|
|
608
950
|
}
|
|
609
951
|
case 'module_info': {
|
|
610
952
|
const moduleArgs = validateModuleInfoArgs(args);
|
|
953
|
+
const cachedModule = sessionCache?.get('module_info', moduleArgs);
|
|
954
|
+
if (cachedModule) {
|
|
955
|
+
recordWithTrace({ tool: 'module_info', path: moduleArgs.module, tokensReturned: cachedModule.tokenEstimate, tokensWouldBe: cachedModule.tokensWouldBe ?? cachedModule.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: moduleArgs });
|
|
956
|
+
return cachedModule.result;
|
|
957
|
+
}
|
|
611
958
|
const moduleResult = await handleModuleInfo(moduleArgs, projectRoot, astIndex);
|
|
612
959
|
const moduleText = moduleResult.content[0]?.text ?? '';
|
|
613
|
-
|
|
614
|
-
const moduleWouldBe =
|
|
615
|
-
|
|
960
|
+
const moduleTokens = estimateTokens(moduleText);
|
|
961
|
+
const moduleWouldBe = await estimateFindUsagesWorkflowTokens(moduleResult.meta.files);
|
|
962
|
+
sessionCache?.set('module_info', moduleArgs, moduleResult, { dependsOnAst: true }, moduleTokens, moduleWouldBe || moduleTokens);
|
|
963
|
+
recordWithTrace({ tool: 'module_info', path: moduleArgs.module, tokensReturned: moduleTokens, tokensWouldBe: moduleWouldBe || moduleTokens, timestamp: Date.now(), savingsCategory: 'compression', args: moduleArgs });
|
|
616
964
|
return moduleResult;
|
|
617
965
|
}
|
|
618
966
|
case 'smart_diff': {
|
|
@@ -620,17 +968,34 @@ export async function createServer(projectRoot, options) {
|
|
|
620
968
|
const sdResult = await handleSmartDiff(sdArgs, projectRoot, astIndex);
|
|
621
969
|
const sdText = sdResult.content[0]?.text ?? '';
|
|
622
970
|
const sdTokens = estimateTokens(sdText);
|
|
623
|
-
|
|
971
|
+
recordWithTrace({ tool: 'smart_diff', path: sdArgs.path ?? sdArgs.scope ?? 'unstaged', tokensReturned: sdTokens, tokensWouldBe: sdResult.rawTokens || sdTokens, timestamp: Date.now(), savingsCategory: 'compression', args: sdArgs });
|
|
624
972
|
return { content: sdResult.content };
|
|
625
973
|
}
|
|
626
974
|
case 'explore_area': {
|
|
627
975
|
const eaArgs = validateExploreAreaArgs(args);
|
|
976
|
+
const cachedEa = sessionCache?.get('explore_area', eaArgs);
|
|
977
|
+
if (cachedEa) {
|
|
978
|
+
recordWithTrace({ tool: 'explore_area', path: eaArgs.path, tokensReturned: cachedEa.tokenEstimate, tokensWouldBe: cachedEa.tokensWouldBe ?? cachedEa.tokenEstimate, timestamp: Date.now(), sessionCacheHit: true, savingsCategory: 'cache', args: eaArgs });
|
|
979
|
+
return cachedEa.result;
|
|
980
|
+
}
|
|
628
981
|
const eaResult = await handleExploreArea(eaArgs, projectRoot, astIndex);
|
|
629
982
|
const eaText = eaResult.content[0]?.text ?? '';
|
|
630
983
|
const eaTokens = estimateTokens(eaText);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
984
|
+
const eaWouldBe = await estimateExploreAreaWorkflowTokens(eaResult.meta);
|
|
985
|
+
sessionCache?.set('explore_area', eaArgs, eaResult, {
|
|
986
|
+
files: [resolve(projectRoot, eaArgs.path) + '/'],
|
|
987
|
+
dependsOnAst: true,
|
|
988
|
+
dependsOnGit: true,
|
|
989
|
+
}, eaTokens, eaWouldBe || eaTokens);
|
|
990
|
+
recordWithTrace({
|
|
991
|
+
tool: 'explore_area',
|
|
992
|
+
path: eaArgs.path,
|
|
993
|
+
tokensReturned: eaTokens,
|
|
994
|
+
tokensWouldBe: eaWouldBe || eaTokens,
|
|
995
|
+
timestamp: Date.now(),
|
|
996
|
+
savingsCategory: 'compression',
|
|
997
|
+
args: eaArgs,
|
|
998
|
+
});
|
|
634
999
|
return eaResult;
|
|
635
1000
|
}
|
|
636
1001
|
case 'smart_log': {
|
|
@@ -638,7 +1003,7 @@ export async function createServer(projectRoot, options) {
|
|
|
638
1003
|
const slResult = await handleSmartLog(slArgs, projectRoot);
|
|
639
1004
|
const slText = slResult.content[0]?.text ?? '';
|
|
640
1005
|
const slTokens = estimateTokens(slText);
|
|
641
|
-
|
|
1006
|
+
recordWithTrace({ tool: 'smart_log', path: slArgs.path ?? 'all', tokensReturned: slTokens, tokensWouldBe: slResult.rawTokens || slTokens, timestamp: Date.now(), savingsCategory: 'compression', args: slArgs });
|
|
642
1007
|
return { content: slResult.content };
|
|
643
1008
|
}
|
|
644
1009
|
case 'test_summary': {
|
|
@@ -646,7 +1011,7 @@ export async function createServer(projectRoot, options) {
|
|
|
646
1011
|
const tsResult = await handleTestSummary(tsArgs, projectRoot);
|
|
647
1012
|
const tsText = tsResult.content[0]?.text ?? '';
|
|
648
1013
|
const tsTokens = estimateTokens(tsText);
|
|
649
|
-
|
|
1014
|
+
recordWithTrace({ tool: 'test_summary', path: tsArgs.command, tokensReturned: tsTokens, tokensWouldBe: tsResult.rawTokens || tsTokens, timestamp: Date.now(), savingsCategory: 'compression', args: tsArgs });
|
|
650
1015
|
return { content: tsResult.content };
|
|
651
1016
|
}
|
|
652
1017
|
default:
|
package/dist/types.d.ts
CHANGED
|
@@ -119,6 +119,18 @@ export interface TokenPilotConfig {
|
|
|
119
119
|
checkOnStartup: boolean;
|
|
120
120
|
autoUpdate: boolean;
|
|
121
121
|
};
|
|
122
|
+
sessionCache: {
|
|
123
|
+
enabled: boolean;
|
|
124
|
+
maxEntries: number;
|
|
125
|
+
};
|
|
126
|
+
policies: {
|
|
127
|
+
preferCheapReads: boolean;
|
|
128
|
+
requireReadForEditBeforeEdit: boolean;
|
|
129
|
+
cacheProjectOverview: boolean;
|
|
130
|
+
maxFullFileReads: number;
|
|
131
|
+
warnOnLargeReads: boolean;
|
|
132
|
+
largeReadThreshold: number;
|
|
133
|
+
};
|
|
122
134
|
ignore: string[];
|
|
123
135
|
}
|
|
124
136
|
//# sourceMappingURL=types.d.ts.map
|