token-pilot 0.9.0 → 0.12.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +47 -0
- package/README.md +8 -0
- package/dist/ast-index/binary-manager.d.ts +13 -0
- package/dist/ast-index/binary-manager.js +43 -0
- package/dist/config/defaults.js +4 -0
- package/dist/core/validation.d.ts +29 -0
- package/dist/core/validation.js +88 -0
- package/dist/handlers/explore-area.d.ts +9 -0
- package/dist/handlers/explore-area.js +280 -0
- package/dist/handlers/outline.d.ts +6 -0
- package/dist/handlers/outline.js +3 -2
- package/dist/handlers/smart-diff.d.ts +35 -0
- package/dist/handlers/smart-diff.js +269 -0
- package/dist/handlers/smart-log.d.ts +21 -0
- package/dist/handlers/smart-log.js +200 -0
- package/dist/handlers/test-summary.d.ts +25 -0
- package/dist/handlers/test-summary.js +321 -0
- package/dist/index.js +110 -43
- package/dist/server.js +98 -2
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AstIndexClient } from '../ast-index/client.js';
|
|
2
|
+
import type { SmartDiffArgs } from '../core/validation.js';
|
|
3
|
+
import type { FileStructure } from '../types.js';
|
|
4
|
+
interface FileDiff {
|
|
5
|
+
path: string;
|
|
6
|
+
oldPath?: string;
|
|
7
|
+
addedLines: number;
|
|
8
|
+
removedLines: number;
|
|
9
|
+
hunks: DiffHunk[];
|
|
10
|
+
isBinary: boolean;
|
|
11
|
+
isNew: boolean;
|
|
12
|
+
isDeleted: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface DiffHunk {
|
|
15
|
+
newStart: number;
|
|
16
|
+
newCount: number;
|
|
17
|
+
lines: string[];
|
|
18
|
+
}
|
|
19
|
+
interface SymbolChange {
|
|
20
|
+
name: string;
|
|
21
|
+
kind: string;
|
|
22
|
+
changeType: 'MODIFIED' | 'ADDED' | 'REMOVED';
|
|
23
|
+
lineRange: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function handleSmartDiff(args: SmartDiffArgs, projectRoot: string, astIndex: AstIndexClient): Promise<{
|
|
26
|
+
content: Array<{
|
|
27
|
+
type: 'text';
|
|
28
|
+
text: string;
|
|
29
|
+
}>;
|
|
30
|
+
rawTokens: number;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function parseUnifiedDiff(raw: string): FileDiff[];
|
|
33
|
+
export declare function mapHunksToSymbols(hunks: DiffHunk[], structure: FileStructure): SymbolChange[];
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=smart-diff.d.ts.map
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { estimateTokens } from '../core/token-estimator.js';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
// ──────────────────────────────────────────────
|
|
7
|
+
// Handler
|
|
8
|
+
// ──────────────────────────────────────────────
|
|
9
|
+
const SMALL_DIFF_THRESHOLD = 30;
|
|
10
|
+
const MAX_FILES = 50;
|
|
11
|
+
const MAX_OUTPUT_LINES = 500;
|
|
12
|
+
export async function handleSmartDiff(args, projectRoot, astIndex) {
|
|
13
|
+
// 1. Build git command
|
|
14
|
+
const gitArgs = buildGitArgs(args);
|
|
15
|
+
// 2. Execute git diff
|
|
16
|
+
let rawDiff;
|
|
17
|
+
try {
|
|
18
|
+
const { stdout } = await execFileAsync('git', gitArgs, {
|
|
19
|
+
cwd: projectRoot,
|
|
20
|
+
timeout: 10000,
|
|
21
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
22
|
+
});
|
|
23
|
+
rawDiff = stdout;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
27
|
+
if (msg.includes('not a git repository') || msg.includes('fatal:')) {
|
|
28
|
+
return { content: [{ type: 'text', text: 'Not a git repository. smart_diff requires git.' }], rawTokens: 0 };
|
|
29
|
+
}
|
|
30
|
+
return { content: [{ type: 'text', text: `git diff failed: ${msg}` }], rawTokens: 0 };
|
|
31
|
+
}
|
|
32
|
+
const rawTokens = estimateTokens(rawDiff);
|
|
33
|
+
if (!rawDiff.trim()) {
|
|
34
|
+
const scopeLabel = args.scope ?? 'unstaged';
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: `NO CHANGES (${scopeLabel}): working tree is clean.` }],
|
|
37
|
+
rawTokens: 0,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// 3. Parse unified diff
|
|
41
|
+
const fileDiffs = parseUnifiedDiff(rawDiff);
|
|
42
|
+
if (fileDiffs.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: 'NO CHANGES: diff parsed but no file changes found.' }],
|
|
45
|
+
rawTokens,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// 4. Map hunks to symbols (parallel, capped)
|
|
49
|
+
const filesToProcess = fileDiffs.slice(0, MAX_FILES);
|
|
50
|
+
const symbolChanges = new Map();
|
|
51
|
+
const outlineResults = await Promise.allSettled(filesToProcess
|
|
52
|
+
.filter(f => !f.isBinary && !f.isDeleted)
|
|
53
|
+
.map(async (f) => {
|
|
54
|
+
const absPath = resolve(projectRoot, f.path);
|
|
55
|
+
const structure = await astIndex.outline(absPath);
|
|
56
|
+
return { path: f.path, structure };
|
|
57
|
+
}));
|
|
58
|
+
for (const result of outlineResults) {
|
|
59
|
+
if (result.status === 'fulfilled' && result.value.structure) {
|
|
60
|
+
const { path, structure } = result.value;
|
|
61
|
+
const fd = filesToProcess.find(f => f.path === path);
|
|
62
|
+
if (fd) {
|
|
63
|
+
symbolChanges.set(path, mapHunksToSymbols(fd.hunks, structure));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 5. Format output
|
|
68
|
+
const output = formatSmartDiff(fileDiffs, filesToProcess, symbolChanges, args, rawTokens);
|
|
69
|
+
return { content: [{ type: 'text', text: output }], rawTokens };
|
|
70
|
+
}
|
|
71
|
+
// ──────────────────────────────────────────────
|
|
72
|
+
// Git command builder
|
|
73
|
+
// ──────────────────────────────────────────────
|
|
74
|
+
function buildGitArgs(args) {
|
|
75
|
+
const base = [];
|
|
76
|
+
switch (args.scope) {
|
|
77
|
+
case 'staged':
|
|
78
|
+
base.push('diff', '--cached', '--no-color');
|
|
79
|
+
break;
|
|
80
|
+
case 'commit':
|
|
81
|
+
base.push('show', '--format=', '--no-color', args.ref);
|
|
82
|
+
break;
|
|
83
|
+
case 'branch':
|
|
84
|
+
base.push('diff', '--no-color', `${args.ref}...HEAD`);
|
|
85
|
+
break;
|
|
86
|
+
case 'unstaged':
|
|
87
|
+
default:
|
|
88
|
+
base.push('diff', '--no-color');
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (args.path) {
|
|
92
|
+
base.push('--', args.path);
|
|
93
|
+
}
|
|
94
|
+
return base;
|
|
95
|
+
}
|
|
96
|
+
// ──────────────────────────────────────────────
|
|
97
|
+
// Unified diff parser
|
|
98
|
+
// ──────────────────────────────────────────────
|
|
99
|
+
export function parseUnifiedDiff(raw) {
|
|
100
|
+
const files = [];
|
|
101
|
+
let current = null;
|
|
102
|
+
let currentHunk = null;
|
|
103
|
+
for (const line of raw.split('\n')) {
|
|
104
|
+
// New file
|
|
105
|
+
if (line.startsWith('diff --git ')) {
|
|
106
|
+
if (current)
|
|
107
|
+
files.push(current);
|
|
108
|
+
const match = line.match(/diff --git a\/(.+?) b\/(.+)/);
|
|
109
|
+
current = {
|
|
110
|
+
path: match?.[2] ?? '',
|
|
111
|
+
oldPath: match?.[1] !== match?.[2] ? match?.[1] : undefined,
|
|
112
|
+
addedLines: 0,
|
|
113
|
+
removedLines: 0,
|
|
114
|
+
hunks: [],
|
|
115
|
+
isBinary: false,
|
|
116
|
+
isNew: false,
|
|
117
|
+
isDeleted: false,
|
|
118
|
+
};
|
|
119
|
+
currentHunk = null;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!current)
|
|
123
|
+
continue;
|
|
124
|
+
if (line.startsWith('new file mode')) {
|
|
125
|
+
current.isNew = true;
|
|
126
|
+
}
|
|
127
|
+
else if (line.startsWith('deleted file mode')) {
|
|
128
|
+
current.isDeleted = true;
|
|
129
|
+
}
|
|
130
|
+
else if (line.startsWith('Binary files')) {
|
|
131
|
+
current.isBinary = true;
|
|
132
|
+
}
|
|
133
|
+
else if (line.startsWith('rename from ')) {
|
|
134
|
+
current.oldPath = line.slice(12);
|
|
135
|
+
}
|
|
136
|
+
else if (line.startsWith('@@ ')) {
|
|
137
|
+
const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
138
|
+
currentHunk = {
|
|
139
|
+
newStart: match ? parseInt(match[1], 10) : 0,
|
|
140
|
+
newCount: match?.[2] ? parseInt(match[2], 10) : 1,
|
|
141
|
+
lines: [],
|
|
142
|
+
};
|
|
143
|
+
current.hunks.push(currentHunk);
|
|
144
|
+
}
|
|
145
|
+
else if (currentHunk) {
|
|
146
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
147
|
+
current.addedLines++;
|
|
148
|
+
currentHunk.lines.push(line);
|
|
149
|
+
}
|
|
150
|
+
else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
151
|
+
current.removedLines++;
|
|
152
|
+
currentHunk.lines.push(line);
|
|
153
|
+
}
|
|
154
|
+
else if (line.startsWith(' ')) {
|
|
155
|
+
currentHunk.lines.push(line);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (current)
|
|
160
|
+
files.push(current);
|
|
161
|
+
return files;
|
|
162
|
+
}
|
|
163
|
+
// ──────────────────────────────────────────────
|
|
164
|
+
// Symbol mapping
|
|
165
|
+
// ──────────────────────────────────────────────
|
|
166
|
+
function flattenSymbols(symbols, prefix = '') {
|
|
167
|
+
const result = [];
|
|
168
|
+
for (const sym of symbols) {
|
|
169
|
+
const name = prefix ? `${prefix}.${sym.name}` : sym.name;
|
|
170
|
+
result.push({
|
|
171
|
+
name,
|
|
172
|
+
kind: sym.kind,
|
|
173
|
+
start: sym.location.startLine,
|
|
174
|
+
end: sym.location.endLine,
|
|
175
|
+
});
|
|
176
|
+
if (sym.children.length > 0) {
|
|
177
|
+
result.push(...flattenSymbols(sym.children, sym.kind === 'class' || sym.kind === 'interface' ? sym.name : ''));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
export function mapHunksToSymbols(hunks, structure) {
|
|
183
|
+
const allSymbols = flattenSymbols(structure.symbols);
|
|
184
|
+
const changedSymbols = new Map();
|
|
185
|
+
// Classify hunks: all-added, all-removed, or mixed
|
|
186
|
+
const hasAdded = hunks.some(h => h.lines.some(l => l.startsWith('+')));
|
|
187
|
+
const hasRemoved = hunks.some(h => h.lines.some(l => l.startsWith('-')));
|
|
188
|
+
for (const hunk of hunks) {
|
|
189
|
+
const hunkStart = hunk.newStart;
|
|
190
|
+
const hunkEnd = hunk.newCount > 0
|
|
191
|
+
? hunk.newStart + hunk.newCount - 1
|
|
192
|
+
: hunk.newStart; // pure deletion: use newStart as point
|
|
193
|
+
for (const sym of allSymbols) {
|
|
194
|
+
if (hunkStart <= sym.end && hunkEnd >= sym.start) {
|
|
195
|
+
if (!changedSymbols.has(sym.name)) {
|
|
196
|
+
// Determine changeType from hunk content
|
|
197
|
+
let changeType = 'MODIFIED';
|
|
198
|
+
if (hasAdded && !hasRemoved)
|
|
199
|
+
changeType = 'ADDED';
|
|
200
|
+
else if (hasRemoved && !hasAdded)
|
|
201
|
+
changeType = 'REMOVED';
|
|
202
|
+
changedSymbols.set(sym.name, {
|
|
203
|
+
name: sym.name,
|
|
204
|
+
kind: sym.kind,
|
|
205
|
+
changeType,
|
|
206
|
+
lineRange: `[L${sym.start}-${sym.end}]`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return Array.from(changedSymbols.values());
|
|
213
|
+
}
|
|
214
|
+
// ──────────────────────────────────────────────
|
|
215
|
+
// Output formatter
|
|
216
|
+
// ──────────────────────────────────────────────
|
|
217
|
+
function formatSmartDiff(allFiles, processedFiles, symbolChanges, args, rawTokens) {
|
|
218
|
+
const totalAdded = allFiles.reduce((s, f) => s + f.addedLines, 0);
|
|
219
|
+
const totalRemoved = allFiles.reduce((s, f) => s + f.removedLines, 0);
|
|
220
|
+
const scopeLabel = args.scope ?? 'unstaged';
|
|
221
|
+
const lines = [];
|
|
222
|
+
lines.push(`CHANGES: ${allFiles.length} file${allFiles.length !== 1 ? 's' : ''}, +${totalAdded} -${totalRemoved} (${scopeLabel})`);
|
|
223
|
+
lines.push('');
|
|
224
|
+
for (const fd of processedFiles) {
|
|
225
|
+
if (lines.length >= MAX_OUTPUT_LINES) {
|
|
226
|
+
lines.push(`... truncated (${allFiles.length - processedFiles.indexOf(fd)} more files)`);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
// File header
|
|
230
|
+
const changeLabel = fd.isNew ? ' [NEW]' : fd.isDeleted ? ' [DELETED]' : '';
|
|
231
|
+
const renameLabel = fd.oldPath ? ` (renamed from ${fd.oldPath})` : '';
|
|
232
|
+
const binaryLabel = fd.isBinary ? ' [BINARY]' : '';
|
|
233
|
+
lines.push(`${fd.path} (+${fd.addedLines} -${fd.removedLines})${changeLabel}${renameLabel}${binaryLabel}`);
|
|
234
|
+
if (fd.isBinary) {
|
|
235
|
+
lines.push('');
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// Symbol changes
|
|
239
|
+
const symbols = symbolChanges.get(fd.path);
|
|
240
|
+
if (symbols && symbols.length > 0) {
|
|
241
|
+
for (const sc of symbols) {
|
|
242
|
+
const parens = ['function', 'method'].includes(sc.kind) ? '()' : '';
|
|
243
|
+
lines.push(` ${sc.changeType}: ${sc.name}${parens} ${sc.lineRange}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Small diff: include actual hunks
|
|
247
|
+
const totalHunkLines = fd.hunks.reduce((s, h) => s + h.lines.length, 0);
|
|
248
|
+
if (totalHunkLines <= SMALL_DIFF_THRESHOLD && totalHunkLines > 0) {
|
|
249
|
+
for (const hunk of fd.hunks) {
|
|
250
|
+
lines.push(` @@ L${hunk.newStart}`);
|
|
251
|
+
for (const hl of hunk.lines) {
|
|
252
|
+
lines.push(` ${hl}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (totalHunkLines > SMALL_DIFF_THRESHOLD) {
|
|
257
|
+
lines.push(` (${totalHunkLines} lines changed — use read_symbol for details)`);
|
|
258
|
+
}
|
|
259
|
+
lines.push('');
|
|
260
|
+
}
|
|
261
|
+
if (allFiles.length > MAX_FILES) {
|
|
262
|
+
lines.push(`Showing ${MAX_FILES} of ${allFiles.length} changed files. Use path filter to narrow.`);
|
|
263
|
+
lines.push('');
|
|
264
|
+
}
|
|
265
|
+
lines.push(`HINT: Use read_symbol(path, symbol) to see full changed code, read_diff(path) for line-level diff.`);
|
|
266
|
+
lines.push(`RAW DIFF: ~${rawTokens} tokens → smart_diff: ~${estimateTokens(lines.join('\n'))} tokens`);
|
|
267
|
+
return lines.join('\n');
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=smart-diff.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SmartLogArgs } from '../core/validation.js';
|
|
2
|
+
export interface LogEntry {
|
|
3
|
+
hash: string;
|
|
4
|
+
date: string;
|
|
5
|
+
author: string;
|
|
6
|
+
message: string;
|
|
7
|
+
category: 'feat' | 'fix' | 'refactor' | 'docs' | 'test' | 'chore' | 'style' | 'perf' | 'other';
|
|
8
|
+
files: string[];
|
|
9
|
+
insertions: number;
|
|
10
|
+
deletions: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function handleSmartLog(args: SmartLogArgs, projectRoot: string): Promise<{
|
|
13
|
+
content: Array<{
|
|
14
|
+
type: 'text';
|
|
15
|
+
text: string;
|
|
16
|
+
}>;
|
|
17
|
+
rawTokens: number;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function parseGitLog(raw: string): LogEntry[];
|
|
20
|
+
export declare function categorizeCommit(message: string): LogEntry['category'];
|
|
21
|
+
//# sourceMappingURL=smart-log.d.ts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { estimateTokens } from '../core/token-estimator.js';
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
// ──────────────────────────────────────────────
|
|
6
|
+
// Constants
|
|
7
|
+
// ──────────────────────────────────────────────
|
|
8
|
+
const RECORD_SEPARATOR = '<<<SEP>>>';
|
|
9
|
+
const FIELD_SEPARATOR = '<<<F>>>';
|
|
10
|
+
const MAX_COUNT = 50;
|
|
11
|
+
// ──────────────────────────────────────────────
|
|
12
|
+
// Handler
|
|
13
|
+
// ──────────────────────────────────────────────
|
|
14
|
+
export async function handleSmartLog(args, projectRoot) {
|
|
15
|
+
const count = Math.min(args.count ?? 10, MAX_COUNT);
|
|
16
|
+
const ref = args.ref ?? 'HEAD';
|
|
17
|
+
// Build git log command with --numstat for file stats
|
|
18
|
+
const gitArgs = [
|
|
19
|
+
'log',
|
|
20
|
+
`--format=${RECORD_SEPARATOR}%h${FIELD_SEPARATOR}%ad${FIELD_SEPARATOR}%an${FIELD_SEPARATOR}%s`,
|
|
21
|
+
'--date=short',
|
|
22
|
+
'--numstat',
|
|
23
|
+
`-n`, `${count}`,
|
|
24
|
+
'--', ref,
|
|
25
|
+
];
|
|
26
|
+
if (args.path) {
|
|
27
|
+
gitArgs.push('--', args.path);
|
|
28
|
+
}
|
|
29
|
+
let rawOutput;
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await execFileAsync('git', gitArgs, {
|
|
32
|
+
cwd: projectRoot,
|
|
33
|
+
timeout: 10000,
|
|
34
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
35
|
+
});
|
|
36
|
+
rawOutput = stdout;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: `git log failed: ${msg}` }],
|
|
42
|
+
rawTokens: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (!rawOutput.trim()) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: 'text', text: 'No commits found.' }],
|
|
48
|
+
rawTokens: 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const rawTokens = estimateTokens(rawOutput);
|
|
52
|
+
const entries = parseGitLog(rawOutput);
|
|
53
|
+
const formatted = formatSmartLog(entries, args.path);
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: 'text', text: formatted }],
|
|
56
|
+
rawTokens,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// ──────────────────────────────────────────────
|
|
60
|
+
// Parser
|
|
61
|
+
// ──────────────────────────────────────────────
|
|
62
|
+
export function parseGitLog(raw) {
|
|
63
|
+
const records = raw.split(RECORD_SEPARATOR).filter(r => r.trim());
|
|
64
|
+
const entries = [];
|
|
65
|
+
for (const record of records) {
|
|
66
|
+
const lines = record.trim().split('\n');
|
|
67
|
+
if (lines.length === 0)
|
|
68
|
+
continue;
|
|
69
|
+
const headerLine = lines[0];
|
|
70
|
+
const fields = headerLine.split(FIELD_SEPARATOR);
|
|
71
|
+
if (fields.length < 4)
|
|
72
|
+
continue;
|
|
73
|
+
const [hash, date, author, message] = fields;
|
|
74
|
+
// Parse numstat lines (insertions\tdeletions\tfile)
|
|
75
|
+
const files = [];
|
|
76
|
+
let insertions = 0;
|
|
77
|
+
let deletions = 0;
|
|
78
|
+
for (let i = 1; i < lines.length; i++) {
|
|
79
|
+
const line = lines[i].trim();
|
|
80
|
+
if (!line)
|
|
81
|
+
continue;
|
|
82
|
+
const parts = line.split('\t');
|
|
83
|
+
if (parts.length >= 3) {
|
|
84
|
+
const ins = parts[0] === '-' ? 0 : parseInt(parts[0], 10) || 0;
|
|
85
|
+
const del = parts[1] === '-' ? 0 : parseInt(parts[1], 10) || 0;
|
|
86
|
+
insertions += ins;
|
|
87
|
+
deletions += del;
|
|
88
|
+
files.push(parts.slice(2).join('\t'));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
entries.push({
|
|
92
|
+
hash,
|
|
93
|
+
date,
|
|
94
|
+
author,
|
|
95
|
+
message,
|
|
96
|
+
category: categorizeCommit(message),
|
|
97
|
+
files,
|
|
98
|
+
insertions,
|
|
99
|
+
deletions,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return entries;
|
|
103
|
+
}
|
|
104
|
+
// ──────────────────────────────────────────────
|
|
105
|
+
// Categorizer
|
|
106
|
+
// ──────────────────────────────────────────────
|
|
107
|
+
export function categorizeCommit(message) {
|
|
108
|
+
const lower = message.toLowerCase();
|
|
109
|
+
// Conventional commits prefix
|
|
110
|
+
if (/^feat[:(!\s]/.test(lower))
|
|
111
|
+
return 'feat';
|
|
112
|
+
if (/^fix[:(!\s]/.test(lower))
|
|
113
|
+
return 'fix';
|
|
114
|
+
if (/^refactor[:(!\s]/.test(lower))
|
|
115
|
+
return 'refactor';
|
|
116
|
+
if (/^docs?[:(!\s]/.test(lower))
|
|
117
|
+
return 'docs';
|
|
118
|
+
if (/^tests?[:(!\s]/.test(lower))
|
|
119
|
+
return 'test';
|
|
120
|
+
if (/^chore[:(!\s]/.test(lower))
|
|
121
|
+
return 'chore';
|
|
122
|
+
if (/^style[:(!\s]/.test(lower))
|
|
123
|
+
return 'style';
|
|
124
|
+
if (/^perf[:(!\s]/.test(lower))
|
|
125
|
+
return 'perf';
|
|
126
|
+
// Version bumps
|
|
127
|
+
if (/^v?\d+\.\d+/.test(lower))
|
|
128
|
+
return 'feat';
|
|
129
|
+
// Keyword heuristics with word boundaries (order matters — more specific first)
|
|
130
|
+
if (/\b(fix|bug|hotfix)\b/.test(lower) || /\bpatch\b/.test(lower))
|
|
131
|
+
return 'fix';
|
|
132
|
+
if (/\b(tests?|specs?|coverage)\b/.test(lower))
|
|
133
|
+
return 'test';
|
|
134
|
+
if (/\b(refactor|restructure|rename|extract)\b/.test(lower) || /\bmove\b/.test(lower))
|
|
135
|
+
return 'refactor';
|
|
136
|
+
if (/\b(docs?|documentation|readme|changelog)\b/.test(lower))
|
|
137
|
+
return 'docs';
|
|
138
|
+
if (/\b(add|new|implement|feature)\b/.test(lower))
|
|
139
|
+
return 'feat';
|
|
140
|
+
if (/\b(style|format|lint)\b/.test(lower))
|
|
141
|
+
return 'style';
|
|
142
|
+
if (/\b(perf|optimiz\w*|speed|faster?)\b/.test(lower))
|
|
143
|
+
return 'perf';
|
|
144
|
+
if (/\b(chore|bump|deps)\b/.test(lower) || /\bci\b/.test(lower) || /\bbuild\b/.test(lower))
|
|
145
|
+
return 'chore';
|
|
146
|
+
return 'other';
|
|
147
|
+
}
|
|
148
|
+
// ──────────────────────────────────────────────
|
|
149
|
+
// Formatter
|
|
150
|
+
// ──────────────────────────────────────────────
|
|
151
|
+
function formatSmartLog(entries, pathFilter) {
|
|
152
|
+
if (entries.length === 0)
|
|
153
|
+
return 'No commits found.';
|
|
154
|
+
const lines = [];
|
|
155
|
+
// Header
|
|
156
|
+
const filterInfo = pathFilter ? ` (filtered: ${pathFilter})` : '';
|
|
157
|
+
lines.push(`GIT LOG: ${entries.length} commits${filterInfo}`);
|
|
158
|
+
lines.push('');
|
|
159
|
+
// Summary stats
|
|
160
|
+
const authors = new Map();
|
|
161
|
+
const categories = new Map();
|
|
162
|
+
let totalIns = 0;
|
|
163
|
+
let totalDel = 0;
|
|
164
|
+
for (const e of entries) {
|
|
165
|
+
authors.set(e.author, (authors.get(e.author) ?? 0) + 1);
|
|
166
|
+
categories.set(e.category, (categories.get(e.category) ?? 0) + 1);
|
|
167
|
+
totalIns += e.insertions;
|
|
168
|
+
totalDel += e.deletions;
|
|
169
|
+
}
|
|
170
|
+
// Category summary
|
|
171
|
+
const catParts = [];
|
|
172
|
+
for (const [cat, count] of Array.from(categories.entries()).sort((a, b) => b[1] - a[1])) {
|
|
173
|
+
catParts.push(`${cat}:${count}`);
|
|
174
|
+
}
|
|
175
|
+
lines.push(`BREAKDOWN: ${catParts.join(', ')} | +${totalIns}/-${totalDel} lines`);
|
|
176
|
+
// Authors
|
|
177
|
+
const authorParts = Array.from(authors.entries())
|
|
178
|
+
.sort((a, b) => b[1] - a[1])
|
|
179
|
+
.slice(0, 5)
|
|
180
|
+
.map(([name, cnt]) => authors.size > 1 ? `${name} (${cnt})` : name);
|
|
181
|
+
lines.push(`AUTHORS: ${authorParts.join(', ')}`);
|
|
182
|
+
lines.push('');
|
|
183
|
+
// Entries
|
|
184
|
+
for (const e of entries) {
|
|
185
|
+
const filesSummary = e.files.length <= 3
|
|
186
|
+
? e.files.join(', ')
|
|
187
|
+
: `${e.files.slice(0, 3).join(', ')} +${e.files.length - 3} more`;
|
|
188
|
+
const stats = e.insertions + e.deletions > 0
|
|
189
|
+
? ` (+${e.insertions}/-${e.deletions})`
|
|
190
|
+
: '';
|
|
191
|
+
lines.push(`${e.hash} ${e.date} [${e.category}] ${e.message}${stats}`);
|
|
192
|
+
if (e.files.length > 0) {
|
|
193
|
+
lines.push(` → ${filesSummary}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push('HINT: Use smart_diff(scope="commit", ref="<hash>") to see structural changes for a specific commit.');
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=smart-log.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TestSummaryArgs } from '../core/validation.js';
|
|
2
|
+
export interface TestResult {
|
|
3
|
+
total: number;
|
|
4
|
+
passed: number;
|
|
5
|
+
failed: number;
|
|
6
|
+
skipped: number;
|
|
7
|
+
duration?: string;
|
|
8
|
+
failures: FailedTest[];
|
|
9
|
+
suites?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface FailedTest {
|
|
12
|
+
name: string;
|
|
13
|
+
file?: string;
|
|
14
|
+
error: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function handleTestSummary(args: TestSummaryArgs, projectRoot: string): Promise<{
|
|
17
|
+
content: Array<{
|
|
18
|
+
type: 'text';
|
|
19
|
+
text: string;
|
|
20
|
+
}>;
|
|
21
|
+
rawTokens: number;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function detectRunner(command: string, output: string): string;
|
|
24
|
+
export declare function parseTestOutput(output: string, runner: string): TestResult;
|
|
25
|
+
//# sourceMappingURL=test-summary.d.ts.map
|