token-pilot 0.28.2 → 0.28.3
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/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +27 -0
- package/agents/tp-api-surface-tracker.md +1 -1
- package/agents/tp-audit-scanner.md +1 -1
- package/agents/tp-commit-writer.md +1 -1
- package/agents/tp-context-engineer.md +1 -1
- package/agents/tp-dead-code-finder.md +1 -1
- package/agents/tp-debugger.md +1 -1
- package/agents/tp-dep-health.md +1 -1
- package/agents/tp-doc-writer.md +1 -1
- package/agents/tp-history-explorer.md +1 -1
- package/agents/tp-impact-analyzer.md +1 -1
- package/agents/tp-incident-timeline.md +1 -1
- package/agents/tp-incremental-builder.md +1 -1
- package/agents/tp-migration-scout.md +1 -1
- package/agents/tp-onboard.md +1 -1
- package/agents/tp-performance-profiler.md +1 -1
- package/agents/tp-pr-reviewer.md +1 -1
- package/agents/tp-refactor-planner.md +1 -1
- package/agents/tp-review-impact.md +1 -1
- package/agents/tp-run.md +1 -1
- package/agents/tp-session-restorer.md +1 -1
- package/agents/tp-ship-coordinator.md +1 -1
- package/agents/tp-spec-writer.md +1 -1
- package/agents/tp-test-coverage-gapper.md +1 -1
- package/agents/tp-test-triage.md +1 -1
- package/agents/tp-test-writer.md +1 -1
- package/dist/handlers/explore-area.d.ts +3 -3
- package/dist/handlers/explore-area.js +86 -62
- package/package.json +1 -1
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Token Pilot \u2014 save 60-90% tokens when AI reads code",
|
|
9
|
-
"version": "0.28.
|
|
9
|
+
"version": "0.28.3"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "token-pilot",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Reduces token consumption by 60-90% via AST-aware lazy file reading, structural symbol navigation, and cross-session tool-usage analytics. 22 MCP tools + 19 subagents + budget watchdog hooks.",
|
|
16
|
-
"version": "0.28.
|
|
16
|
+
"version": "0.28.3",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Digital-Threads"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.3",
|
|
4
4
|
"description": "Saves 60-90% tokens when AI reads code. AST-aware lazy reading, symbol navigation, cross-session tool-usage analytics, 22 subagents (haiku/sonnet/opus-tiered) with budget watchdog.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Digital-Threads",
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ All notable changes to Token Pilot will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.28.3] - 2026-04-19
|
|
9
|
+
|
|
10
|
+
### Fixed — `explore_area` output size (was −31% savings)
|
|
11
|
+
|
|
12
|
+
Two independent live verification runs — Sonnet 4.6 on v0.28.1 and Opus 4.7 on v0.28.2, both on `docker-local-env` — measured `explore_area` at exactly **−31% savings**: 5,722 tokens returned against a 4,360-token baseline of reading the scanned files raw. That's the opposite of the tool's stated purpose. Root cause: imports analysis + tests listing + git-log tail accumulated on top of the directory outline, pushing the response above what the individual file-reads would have cost.
|
|
13
|
+
|
|
14
|
+
Tightened two caps in `src/handlers/explore-area.ts`:
|
|
15
|
+
|
|
16
|
+
| Constant | Before | After | Effect |
|
|
17
|
+
|---|---:|---:|---|
|
|
18
|
+
| `MAX_IMPORT_FILES` | 20 | **10** | imports panel scans half as many files |
|
|
19
|
+
| `MAX_OUTPUT_LINES` | 500 | **200** | global response cap drops 60 % |
|
|
20
|
+
|
|
21
|
+
The structural overview survives; the tail (detailed per-file imports past the top 10, git-log beyond the first screen) drops. Per-call smoke-test in the dev harness lands around +40–60 % savings, matching what the tool was supposed to deliver.
|
|
22
|
+
|
|
23
|
+
Self-sizing (compare the predicted output against `estimateExploreAreaWorkflowTokens` baseline and trim if exceeded) deferred to v0.29.0 — needs handler + server coordination.
|
|
24
|
+
|
|
25
|
+
### Noted for v0.29.0 (not this release)
|
|
26
|
+
|
|
27
|
+
Composite Bash escape in `PreToolUse:Bash` hook:
|
|
28
|
+
- `;` `&&` `||` `|` + newline separators → detected correctly (verified)
|
|
29
|
+
- `bash -c "cat src/foo.ts"`, `eval "..."`, `for f in *.ts; do cat $f; done` → slip through (quoted / wrapped commands not lexed)
|
|
30
|
+
|
|
31
|
+
Not shipping today because all three escape patterns require advanced shell knowledge and are rare in agent-generated commands. Opus 4.7's v0.28.2 verification confirmed 5/6 TP-blocked on realistic patterns. Fixing `bash -c` properly needs a small shell-tokenizer; worth a focused design pass, not a same-day patch.
|
|
32
|
+
|
|
33
|
+
1019 tests still passing.
|
|
34
|
+
|
|
8
35
|
## [0.28.2] - 2026-04-19
|
|
9
36
|
|
|
10
37
|
### Fixed — plugin hooks were never actually reaching Claude Code
|
package/agents/tp-debugger.md
CHANGED
package/agents/tp-dep-health.md
CHANGED
package/agents/tp-doc-writer.md
CHANGED
package/agents/tp-onboard.md
CHANGED
|
@@ -10,7 +10,7 @@ tools:
|
|
|
10
10
|
- mcp__token-pilot__smart_read
|
|
11
11
|
- mcp__token-pilot__smart_read_many
|
|
12
12
|
- mcp__token-pilot__read_section
|
|
13
|
-
token_pilot_version: "0.28.
|
|
13
|
+
token_pilot_version: "0.28.3"
|
|
14
14
|
token_pilot_body_hash: ae0b86eaffaf34bf283b94b5572481fa8c2d6a2a25193f1173b70bef0fbe1919
|
|
15
15
|
---
|
|
16
16
|
|
package/agents/tp-pr-reviewer.md
CHANGED
package/agents/tp-run.md
CHANGED
package/agents/tp-spec-writer.md
CHANGED
package/agents/tp-test-triage.md
CHANGED
package/agents/tp-test-writer.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AstIndexClient } from
|
|
2
|
-
import type { ExploreAreaArgs } from
|
|
1
|
+
import type { AstIndexClient } from "../ast-index/client.js";
|
|
2
|
+
import type { ExploreAreaArgs } from "../core/validation.js";
|
|
3
3
|
export interface ExploreAreaMeta {
|
|
4
4
|
dir: string;
|
|
5
5
|
codeFiles: string[];
|
|
@@ -11,7 +11,7 @@ export interface ExploreAreaMeta {
|
|
|
11
11
|
}
|
|
12
12
|
export declare function handleExploreArea(args: ExploreAreaArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
|
|
13
13
|
content: Array<{
|
|
14
|
-
type:
|
|
14
|
+
type: "text";
|
|
15
15
|
text: string;
|
|
16
16
|
}>;
|
|
17
17
|
meta: ExploreAreaMeta;
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import { execFile } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { readdir, stat } from
|
|
4
|
-
import { resolve, relative, basename, dirname } from
|
|
5
|
-
import { resolveSafePath } from
|
|
6
|
-
import { outlineDir, CODE_EXTENSIONS } from
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { readdir, stat } from "node:fs/promises";
|
|
4
|
+
import { resolve, relative, basename, dirname } from "node:path";
|
|
5
|
+
import { resolveSafePath } from "../core/validation.js";
|
|
6
|
+
import { outlineDir, CODE_EXTENSIONS } from "./outline.js";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
// ──────────────────────────────────────────────
|
|
9
9
|
// Constants
|
|
10
10
|
// ──────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// v0.28.3 — tightened from 20/500. Two independent verification runs
|
|
12
|
+
// (Sonnet 4.6 + Opus 4.7 on docker-local-env) measured explore_area at
|
|
13
|
+
// -31% savings — output was larger than reading scanned files raw.
|
|
14
|
+
// Root cause: imports + tests + git log accumulated on top of the
|
|
15
|
+
// directory outline. Halving both caps keeps the structural overview
|
|
16
|
+
// while dropping the tail nobody actually reads. Self-sizing (compare
|
|
17
|
+
// against baseline and trim if exceeded) deferred to v0.29.0.
|
|
18
|
+
const MAX_IMPORT_FILES = 10;
|
|
19
|
+
const MAX_OUTPUT_LINES = 200;
|
|
13
20
|
// ──────────────────────────────────────────────
|
|
14
21
|
// Handler
|
|
15
22
|
// ──────────────────────────────────────────────
|
|
@@ -19,7 +26,7 @@ export async function handleExploreArea(args, projectRoot, astIndex) {
|
|
|
19
26
|
const pathStat = await stat(absPath).catch(() => null);
|
|
20
27
|
if (!pathStat) {
|
|
21
28
|
return {
|
|
22
|
-
content: [{ type:
|
|
29
|
+
content: [{ type: "text", text: `Path "${args.path}" not found.` }],
|
|
23
30
|
meta: {
|
|
24
31
|
dir: args.path,
|
|
25
32
|
codeFiles: [],
|
|
@@ -34,28 +41,36 @@ export async function handleExploreArea(args, projectRoot, astIndex) {
|
|
|
34
41
|
if (!pathStat.isDirectory()) {
|
|
35
42
|
absPath = dirname(absPath);
|
|
36
43
|
}
|
|
37
|
-
const relDir = relative(projectRoot, absPath) ||
|
|
38
|
-
const include = args.include ?? [
|
|
44
|
+
const relDir = relative(projectRoot, absPath) || ".";
|
|
45
|
+
const include = args.include ?? ["outline", "imports", "tests", "changes"];
|
|
39
46
|
// Collect code files for import/test analysis
|
|
40
47
|
const codeFiles = await listCodeFiles(absPath);
|
|
41
48
|
// Run all sections in parallel
|
|
42
49
|
const [outlineSection, importsSection, testsSection, changesSection] = await Promise.allSettled([
|
|
43
|
-
include.includes(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
include.includes(
|
|
50
|
+
include.includes("outline")
|
|
51
|
+
? buildOutlineSection(absPath, projectRoot, astIndex)
|
|
52
|
+
: Promise.resolve(null),
|
|
53
|
+
include.includes("imports")
|
|
54
|
+
? buildImportsSection(codeFiles, absPath, projectRoot, astIndex)
|
|
55
|
+
: Promise.resolve(null),
|
|
56
|
+
include.includes("tests")
|
|
57
|
+
? buildTestsSection(codeFiles, absPath, projectRoot)
|
|
58
|
+
: Promise.resolve(null),
|
|
59
|
+
include.includes("changes")
|
|
60
|
+
? buildChangesSection(relDir, projectRoot)
|
|
61
|
+
: Promise.resolve(null),
|
|
47
62
|
]);
|
|
48
63
|
// Assemble output
|
|
49
64
|
const lines = [];
|
|
50
65
|
const subdirCount = await countSubdirs(absPath);
|
|
51
|
-
lines.push(`AREA: ${relDir}/ (${codeFiles.length} code files${subdirCount > 0 ? `, ${subdirCount} subdirs` :
|
|
52
|
-
lines.push(
|
|
66
|
+
lines.push(`AREA: ${relDir}/ (${codeFiles.length} code files${subdirCount > 0 ? `, ${subdirCount} subdirs` : ""})`);
|
|
67
|
+
lines.push("");
|
|
53
68
|
// Outline
|
|
54
69
|
const outlineLines = extractResult(outlineSection);
|
|
55
70
|
if (outlineLines) {
|
|
56
|
-
lines.push(
|
|
71
|
+
lines.push("STRUCTURE:");
|
|
57
72
|
lines.push(...outlineLines);
|
|
58
|
-
lines.push(
|
|
73
|
+
lines.push("");
|
|
59
74
|
}
|
|
60
75
|
// Imports
|
|
61
76
|
const importLines = extractResult(importsSection)?.lines ?? null;
|
|
@@ -75,11 +90,11 @@ export async function handleExploreArea(args, projectRoot, astIndex) {
|
|
|
75
90
|
// Truncate if needed
|
|
76
91
|
if (lines.length > MAX_OUTPUT_LINES) {
|
|
77
92
|
lines.length = MAX_OUTPUT_LINES;
|
|
78
|
-
lines.push(
|
|
93
|
+
lines.push("... truncated. Use outline() on specific subdirectories for details.");
|
|
79
94
|
}
|
|
80
|
-
lines.push(
|
|
95
|
+
lines.push("HINT: Use smart_read(file) for details, read_symbol(path, symbol) for source code, find_usages(symbol) for references.");
|
|
81
96
|
return {
|
|
82
|
-
content: [{ type:
|
|
97
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
83
98
|
meta: {
|
|
84
99
|
dir: relDir,
|
|
85
100
|
codeFiles: codeFiles.map((file) => relative(projectRoot, file)).sort(),
|
|
@@ -103,43 +118,47 @@ async function buildOutlineSection(absPath, projectRoot, astIndex) {
|
|
|
103
118
|
// Imports section — aggregate external deps + who imports this area
|
|
104
119
|
// ──────────────────────────────────────────────
|
|
105
120
|
async function buildImportsSection(codeFiles, absPath, projectRoot, astIndex) {
|
|
106
|
-
if (!astIndex.isAvailable() ||
|
|
121
|
+
if (!astIndex.isAvailable() ||
|
|
122
|
+
astIndex.isDisabled() ||
|
|
123
|
+
astIndex.isOversized()) {
|
|
107
124
|
return { lines: [], internalDeps: [], importedBy: [], externalDeps: [] };
|
|
108
125
|
}
|
|
109
126
|
const filesToAnalyze = codeFiles.slice(0, MAX_IMPORT_FILES);
|
|
110
127
|
const externalDeps = new Set();
|
|
111
128
|
const internalDeps = new Set();
|
|
112
|
-
const relDir = relative(projectRoot, absPath) ||
|
|
129
|
+
const relDir = relative(projectRoot, absPath) || ".";
|
|
113
130
|
// Get imports for each file
|
|
114
|
-
const importResults = await Promise.allSettled(filesToAnalyze.map(f => astIndex.fileImports(f)));
|
|
131
|
+
const importResults = await Promise.allSettled(filesToAnalyze.map((f) => astIndex.fileImports(f)));
|
|
115
132
|
for (const result of importResults) {
|
|
116
|
-
if (result.status !==
|
|
133
|
+
if (result.status !== "fulfilled" || !result.value)
|
|
117
134
|
continue;
|
|
118
135
|
for (const imp of result.value) {
|
|
119
136
|
const source = imp.source;
|
|
120
137
|
if (!source)
|
|
121
138
|
continue;
|
|
122
|
-
if (source.startsWith(
|
|
139
|
+
if (source.startsWith(".") || source.startsWith("/")) {
|
|
123
140
|
// Internal import — track if it's outside this area
|
|
124
141
|
const resolved = resolve(absPath, source);
|
|
125
|
-
if (!resolved.startsWith(absPath +
|
|
126
|
-
const relImport = relative(projectRoot, resolved).replace(/\.[^.]+$/,
|
|
142
|
+
if (!resolved.startsWith(absPath + "/") && resolved !== absPath) {
|
|
143
|
+
const relImport = relative(projectRoot, resolved).replace(/\.[^.]+$/, "");
|
|
127
144
|
internalDeps.add(relImport);
|
|
128
145
|
}
|
|
129
146
|
}
|
|
130
147
|
else {
|
|
131
148
|
// External package
|
|
132
|
-
const pkg = source.startsWith(
|
|
149
|
+
const pkg = source.startsWith("@")
|
|
150
|
+
? source.split("/").slice(0, 2).join("/")
|
|
151
|
+
: source.split("/")[0];
|
|
133
152
|
externalDeps.add(pkg);
|
|
134
153
|
}
|
|
135
154
|
}
|
|
136
155
|
}
|
|
137
156
|
// Find who imports files from this area (reverse dependencies)
|
|
138
157
|
const importedBy = new Set();
|
|
139
|
-
const fileBasenames = filesToAnalyze.map(f => basename(f).replace(/\.[^.]+$/,
|
|
140
|
-
const refResults = await Promise.allSettled(fileBasenames.slice(0, 10).map(name => astIndex.refs(name, 10)));
|
|
158
|
+
const fileBasenames = filesToAnalyze.map((f) => basename(f).replace(/\.[^.]+$/, ""));
|
|
159
|
+
const refResults = await Promise.allSettled(fileBasenames.slice(0, 10).map((name) => astIndex.refs(name, 10)));
|
|
141
160
|
for (const result of refResults) {
|
|
142
|
-
if (result.status !==
|
|
161
|
+
if (result.status !== "fulfilled" || !result.value)
|
|
143
162
|
continue;
|
|
144
163
|
const refs = result.value;
|
|
145
164
|
if (refs.imports) {
|
|
@@ -149,8 +168,8 @@ async function buildImportsSection(codeFiles, absPath, projectRoot, astIndex) {
|
|
|
149
168
|
continue;
|
|
150
169
|
const relFile = relative(projectRoot, impFile);
|
|
151
170
|
// Only include files outside this area
|
|
152
|
-
if (!relFile.startsWith(relDir +
|
|
153
|
-
importedBy.add(relFile.replace(/\.[^.]+$/,
|
|
171
|
+
if (!relFile.startsWith(relDir + "/") && relFile !== relDir) {
|
|
172
|
+
importedBy.add(relFile.replace(/\.[^.]+$/, ""));
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
175
|
}
|
|
@@ -158,18 +177,18 @@ async function buildImportsSection(codeFiles, absPath, projectRoot, astIndex) {
|
|
|
158
177
|
const lines = [];
|
|
159
178
|
if (externalDeps.size > 0) {
|
|
160
179
|
const deps = Array.from(externalDeps).sort().slice(0, 20);
|
|
161
|
-
lines.push(`IMPORTS: ${deps.join(
|
|
180
|
+
lines.push(`IMPORTS: ${deps.join(", ")}${externalDeps.size > 20 ? ` ... (${externalDeps.size} total)` : ""}`);
|
|
162
181
|
}
|
|
163
182
|
if (internalDeps.size > 0) {
|
|
164
183
|
const deps = Array.from(internalDeps).sort().slice(0, 10);
|
|
165
|
-
lines.push(`INTERNAL DEPS: ${deps.join(
|
|
184
|
+
lines.push(`INTERNAL DEPS: ${deps.join(", ")}${internalDeps.size > 10 ? ` ... (${internalDeps.size} total)` : ""}`);
|
|
166
185
|
}
|
|
167
186
|
if (importedBy.size > 0) {
|
|
168
187
|
const importers = Array.from(importedBy).sort().slice(0, 10);
|
|
169
|
-
lines.push(`IMPORTED BY: ${importers.join(
|
|
188
|
+
lines.push(`IMPORTED BY: ${importers.join(", ")}${importedBy.size > 10 ? ` ... (${importedBy.size} total)` : ""}`);
|
|
170
189
|
}
|
|
171
190
|
if (lines.length > 0)
|
|
172
|
-
lines.push(
|
|
191
|
+
lines.push("");
|
|
173
192
|
return {
|
|
174
193
|
lines,
|
|
175
194
|
internalDeps: Array.from(internalDeps).sort(),
|
|
@@ -182,18 +201,18 @@ async function buildImportsSection(codeFiles, absPath, projectRoot, astIndex) {
|
|
|
182
201
|
// ──────────────────────────────────────────────
|
|
183
202
|
async function buildTestsSection(codeFiles, absPath, projectRoot) {
|
|
184
203
|
const testFiles = [];
|
|
185
|
-
const areaFileNames = new Set(codeFiles.map(f => basename(f).replace(/\.[^.]+$/,
|
|
204
|
+
const areaFileNames = new Set(codeFiles.map((f) => basename(f).replace(/\.[^.]+$/, "")));
|
|
186
205
|
// Scan for test files: check area dir + common test dirs
|
|
187
206
|
const dirsToScan = [absPath];
|
|
188
207
|
// Check for sibling __tests__ or tests directory
|
|
189
208
|
const parent = dirname(absPath);
|
|
190
209
|
const areaName = basename(absPath);
|
|
191
210
|
const testDirCandidates = [
|
|
192
|
-
resolve(absPath,
|
|
193
|
-
resolve(absPath,
|
|
194
|
-
resolve(absPath,
|
|
195
|
-
resolve(parent,
|
|
196
|
-
resolve(parent,
|
|
211
|
+
resolve(absPath, "__tests__"),
|
|
212
|
+
resolve(absPath, "tests"),
|
|
213
|
+
resolve(absPath, "test"),
|
|
214
|
+
resolve(parent, "__tests__", areaName),
|
|
215
|
+
resolve(parent, "tests", areaName),
|
|
197
216
|
];
|
|
198
217
|
for (const testDir of testDirCandidates) {
|
|
199
218
|
const testDirStat = await stat(testDir).catch(() => null);
|
|
@@ -203,9 +222,9 @@ async function buildTestsSection(codeFiles, absPath, projectRoot) {
|
|
|
203
222
|
}
|
|
204
223
|
// Also check project-level test directories
|
|
205
224
|
const projectTestDirs = [
|
|
206
|
-
resolve(projectRoot,
|
|
207
|
-
resolve(projectRoot,
|
|
208
|
-
resolve(projectRoot,
|
|
225
|
+
resolve(projectRoot, "tests"),
|
|
226
|
+
resolve(projectRoot, "test"),
|
|
227
|
+
resolve(projectRoot, "__tests__"),
|
|
209
228
|
];
|
|
210
229
|
for (const testDir of projectTestDirs) {
|
|
211
230
|
if (dirsToScan.includes(testDir))
|
|
@@ -222,12 +241,15 @@ async function buildTestsSection(codeFiles, absPath, projectRoot) {
|
|
|
222
241
|
if (!entry.isFile())
|
|
223
242
|
continue;
|
|
224
243
|
const name = entry.name;
|
|
225
|
-
if (name.includes(
|
|
244
|
+
if (name.includes(".test.") ||
|
|
245
|
+
name.includes(".spec.") ||
|
|
246
|
+
name.includes("_test.") ||
|
|
247
|
+
name.includes("_spec.")) {
|
|
226
248
|
// Check if this test corresponds to an area file
|
|
227
249
|
const testBase = name
|
|
228
|
-
.replace(/\.(test|spec)\./,
|
|
229
|
-
.replace(/_(test|spec)\./,
|
|
230
|
-
.replace(/\.[^.]+$/,
|
|
250
|
+
.replace(/\.(test|spec)\./, ".")
|
|
251
|
+
.replace(/_(test|spec)\./, ".")
|
|
252
|
+
.replace(/\.[^.]+$/, "");
|
|
231
253
|
if (areaFileNames.has(testBase) || dir !== absPath) {
|
|
232
254
|
const relPath = relative(projectRoot, resolve(dir, name));
|
|
233
255
|
if (!testFiles.includes(relPath)) {
|
|
@@ -237,13 +259,15 @@ async function buildTestsSection(codeFiles, absPath, projectRoot) {
|
|
|
237
259
|
}
|
|
238
260
|
}
|
|
239
261
|
}
|
|
240
|
-
catch {
|
|
262
|
+
catch {
|
|
263
|
+
/* skip unreadable dirs */
|
|
264
|
+
}
|
|
241
265
|
}
|
|
242
266
|
if (testFiles.length === 0)
|
|
243
267
|
return { lines: [], testFiles: [] };
|
|
244
268
|
const lines = [];
|
|
245
|
-
lines.push(`TESTS: ${testFiles.join(
|
|
246
|
-
lines.push(
|
|
269
|
+
lines.push(`TESTS: ${testFiles.join(", ")}`);
|
|
270
|
+
lines.push("");
|
|
247
271
|
return { lines, testFiles: [...testFiles].sort() };
|
|
248
272
|
}
|
|
249
273
|
// ──────────────────────────────────────────────
|
|
@@ -251,16 +275,16 @@ async function buildTestsSection(codeFiles, absPath, projectRoot) {
|
|
|
251
275
|
// ──────────────────────────────────────────────
|
|
252
276
|
async function buildChangesSection(relDir, projectRoot) {
|
|
253
277
|
try {
|
|
254
|
-
const { stdout } = await execFileAsync(
|
|
278
|
+
const { stdout } = await execFileAsync("git", ["log", "--oneline", "-5", "--", relDir], { cwd: projectRoot, timeout: 5000 });
|
|
255
279
|
if (!stdout.trim())
|
|
256
280
|
return { lines: [], count: 0 };
|
|
257
281
|
const lines = [];
|
|
258
|
-
const commits = stdout.trim().split(
|
|
259
|
-
lines.push(
|
|
282
|
+
const commits = stdout.trim().split("\n");
|
|
283
|
+
lines.push("RECENT CHANGES:");
|
|
260
284
|
for (const line of commits) {
|
|
261
285
|
lines.push(` ${line}`);
|
|
262
286
|
}
|
|
263
|
-
lines.push(
|
|
287
|
+
lines.push("");
|
|
264
288
|
return { lines, count: commits.length };
|
|
265
289
|
}
|
|
266
290
|
catch {
|
|
@@ -271,7 +295,7 @@ async function buildChangesSection(relDir, projectRoot) {
|
|
|
271
295
|
// Helpers
|
|
272
296
|
// ──────────────────────────────────────────────
|
|
273
297
|
function extractResult(settled) {
|
|
274
|
-
if (settled.status ===
|
|
298
|
+
if (settled.status === "fulfilled" && settled.value) {
|
|
275
299
|
return settled.value;
|
|
276
300
|
}
|
|
277
301
|
return null;
|
|
@@ -282,7 +306,7 @@ async function listCodeFiles(dirPath) {
|
|
|
282
306
|
const files = [];
|
|
283
307
|
for (const entry of entries) {
|
|
284
308
|
if (entry.isFile()) {
|
|
285
|
-
const ext = entry.name.split(
|
|
309
|
+
const ext = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
286
310
|
if (CODE_EXTENSIONS.has(ext)) {
|
|
287
311
|
files.push(resolve(dirPath, entry.name));
|
|
288
312
|
}
|
|
@@ -297,7 +321,7 @@ async function listCodeFiles(dirPath) {
|
|
|
297
321
|
async function countSubdirs(dirPath) {
|
|
298
322
|
try {
|
|
299
323
|
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
300
|
-
return entries.filter(e => e.isDirectory()).length;
|
|
324
|
+
return entries.filter((e) => e.isDirectory()).length;
|
|
301
325
|
}
|
|
302
326
|
catch {
|
|
303
327
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-pilot",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.3",
|
|
4
4
|
"description": "Save up to 80% tokens when AI reads code \u2014 MCP server for token-efficient code navigation, AST-aware structural reading instead of dumping full files into context window",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|