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.
@@ -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