pi-rtk 0.1.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,245 @@
1
+ const GIT_COMMANDS = ["git diff", "git status", "git log", "git show", "git stash"];
2
+
3
+ export function isGitCommand(command: string | undefined | null): boolean {
4
+ if (typeof command !== "string" || command.length === 0) {
5
+ return false;
6
+ }
7
+
8
+ const cmdLower = command.toLowerCase();
9
+ return GIT_COMMANDS.some((gc) => cmdLower.startsWith(gc));
10
+ }
11
+
12
+ export function compactDiff(output: string, maxLines: number = 50): string {
13
+ const lines = output.split("\n");
14
+ const result: string[] = [];
15
+ let currentFile = "";
16
+ let added = 0;
17
+ let removed = 0;
18
+ let inHunk = false;
19
+ let hunkLines = 0;
20
+ const maxHunkLines = 10;
21
+
22
+ for (const line of lines) {
23
+ if (result.length >= maxLines) {
24
+ result.push("\n... (more changes truncated)");
25
+ break;
26
+ }
27
+
28
+ // New file
29
+ if (line.startsWith("diff --git")) {
30
+ // Flush previous file stats
31
+ if (currentFile && (added > 0 || removed > 0)) {
32
+ result.push(` +${added} -${removed}`);
33
+ }
34
+
35
+ // Extract filename
36
+ const match = line.match(/diff --git a\/(.+) b\/(.+)/);
37
+ currentFile = match ? match[2] : "unknown";
38
+ result.push(`\nšŸ“„ ${currentFile}`);
39
+ added = 0;
40
+ removed = 0;
41
+ inHunk = false;
42
+ continue;
43
+ }
44
+
45
+ // Hunk header
46
+ if (line.startsWith("@@")) {
47
+ inHunk = true;
48
+ hunkLines = 0;
49
+ const hunkInfo = line.match(/@@ .+ @@/)?.[0] || "@@";
50
+ result.push(` ${hunkInfo}`);
51
+ continue;
52
+ }
53
+
54
+ // Hunk content
55
+ if (inHunk) {
56
+ if (line.startsWith("+") && !line.startsWith("+++")) {
57
+ added++;
58
+ if (hunkLines < maxHunkLines) {
59
+ result.push(` ${line}`);
60
+ hunkLines++;
61
+ }
62
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
63
+ removed++;
64
+ if (hunkLines < maxHunkLines) {
65
+ result.push(` ${line}`);
66
+ hunkLines++;
67
+ }
68
+ } else if (hunkLines < maxHunkLines && !line.startsWith("\\")) {
69
+ if (hunkLines > 0) {
70
+ result.push(` ${line}`);
71
+ hunkLines++;
72
+ }
73
+ }
74
+
75
+ if (hunkLines === maxHunkLines) {
76
+ result.push(" ... (truncated)");
77
+ hunkLines++;
78
+ }
79
+ }
80
+ }
81
+
82
+ // Flush last file stats
83
+ if (currentFile && (added > 0 || removed > 0)) {
84
+ result.push(` +${added} -${removed}`);
85
+ }
86
+
87
+ return result.join("\n");
88
+ }
89
+
90
+ interface StatusStats {
91
+ staged: number;
92
+ modified: number;
93
+ untracked: number;
94
+ conflicts: number;
95
+ stagedFiles: string[];
96
+ modifiedFiles: string[];
97
+ untrackedFiles: string[];
98
+ }
99
+
100
+ export function compactStatus(output: string): string {
101
+ const lines = output.split("\n");
102
+
103
+ if (lines.length === 0 || (lines.length === 1 && lines[0].trim() === "")) {
104
+ return "Clean working tree";
105
+ }
106
+
107
+ const stats: StatusStats = {
108
+ staged: 0,
109
+ modified: 0,
110
+ untracked: 0,
111
+ conflicts: 0,
112
+ stagedFiles: [],
113
+ modifiedFiles: [],
114
+ untrackedFiles: [],
115
+ };
116
+
117
+ let branchName = "";
118
+
119
+ for (const line of lines) {
120
+ // Extract branch name from first line
121
+ if (line.startsWith("##")) {
122
+ const match = line.match(/## (.+)/);
123
+ if (match) {
124
+ branchName = match[1].split("...")[0];
125
+ }
126
+ continue;
127
+ }
128
+
129
+ if (line.length < 3) {
130
+ continue;
131
+ }
132
+
133
+ const status = line.slice(0, 2);
134
+ const filename = line.slice(3);
135
+
136
+ // Parse two-character status
137
+ const indexStatus = status[0];
138
+ const worktreeStatus = status[1];
139
+
140
+ if (["M", "A", "D", "R", "C"].includes(indexStatus)) {
141
+ stats.staged++;
142
+ stats.stagedFiles.push(filename);
143
+ }
144
+
145
+ if (indexStatus === "U") {
146
+ stats.conflicts++;
147
+ }
148
+
149
+ if (["M", "D"].includes(worktreeStatus)) {
150
+ stats.modified++;
151
+ stats.modifiedFiles.push(filename);
152
+ }
153
+
154
+ if (status === "??") {
155
+ stats.untracked++;
156
+ stats.untrackedFiles.push(filename);
157
+ }
158
+ }
159
+
160
+ // Build summary
161
+ let result = `šŸ“Œ ${branchName}\n`;
162
+
163
+ if (stats.staged > 0) {
164
+ result += `āœ… Staged: ${stats.staged} files\n`;
165
+ const shown = stats.stagedFiles.slice(0, 5);
166
+ for (const file of shown) {
167
+ result += ` ${file}\n`;
168
+ }
169
+ if (stats.staged > 5) {
170
+ result += ` ... +${stats.staged - 5} more\n`;
171
+ }
172
+ }
173
+
174
+ if (stats.modified > 0) {
175
+ result += `šŸ“ Modified: ${stats.modified} files\n`;
176
+ const shown = stats.modifiedFiles.slice(0, 5);
177
+ for (const file of shown) {
178
+ result += ` ${file}\n`;
179
+ }
180
+ if (stats.modified > 5) {
181
+ result += ` ... +${stats.modified - 5} more\n`;
182
+ }
183
+ }
184
+
185
+ if (stats.untracked > 0) {
186
+ result += `ā“ Untracked: ${stats.untracked} files\n`;
187
+ const shown = stats.untrackedFiles.slice(0, 3);
188
+ for (const file of shown) {
189
+ result += ` ${file}\n`;
190
+ }
191
+ if (stats.untracked > 3) {
192
+ result += ` ... +${stats.untracked - 3} more\n`;
193
+ }
194
+ }
195
+
196
+ if (stats.conflicts > 0) {
197
+ result += `āš ļø Conflicts: ${stats.conflicts} files\n`;
198
+ }
199
+
200
+ return result.trim();
201
+ }
202
+
203
+ export function compactLog(output: string, limit: number = 20): string {
204
+ const lines = output.split("\n");
205
+ const result: string[] = [];
206
+
207
+ for (const line of lines.slice(0, limit)) {
208
+ if (line.length > 80) {
209
+ result.push(line.slice(0, 77) + "...");
210
+ } else {
211
+ result.push(line);
212
+ }
213
+ }
214
+
215
+ if (lines.length > limit) {
216
+ result.push(`... and ${lines.length - limit} more commits`);
217
+ }
218
+
219
+ return result.join("\n");
220
+ }
221
+
222
+ export function compactGitOutput(
223
+ output: string,
224
+ command: string | undefined | null
225
+ ): string | null {
226
+ if (typeof command !== "string" || !isGitCommand(command)) {
227
+ return null;
228
+ }
229
+
230
+ const cmdLower = command.toLowerCase();
231
+
232
+ if (cmdLower.startsWith("git diff")) {
233
+ return compactDiff(output);
234
+ }
235
+
236
+ if (cmdLower.startsWith("git status")) {
237
+ return compactStatus(output);
238
+ }
239
+
240
+ if (cmdLower.startsWith("git log")) {
241
+ return compactLog(output);
242
+ }
243
+
244
+ return null;
245
+ }
@@ -0,0 +1,16 @@
1
+ // Re-export all techniques
2
+ export { stripAnsi, stripAnsiFast } from "./ansi";
3
+ export { truncate, truncateLines } from "./truncate";
4
+ export { filterBuildOutput, isBuildCommand } from "./build";
5
+ export { aggregateTestOutput, isTestCommand } from "./test-output";
6
+ export { aggregateLinterOutput, isLinterCommand } from "./linter";
7
+ export {
8
+ detectLanguage,
9
+ filterMinimal,
10
+ filterAggressive,
11
+ smartTruncate,
12
+ filterSourceCode,
13
+ type Language,
14
+ } from "./source";
15
+ export { compactDiff, compactStatus, compactLog, compactGitOutput, isGitCommand } from "./git";
16
+ export { groupSearchResults, isSearchCommand } from "./search";
@@ -0,0 +1,191 @@
1
+ const LINTER_COMMANDS = [
2
+ "eslint",
3
+ "prettier",
4
+ "ruff",
5
+ "pylint",
6
+ "mypy",
7
+ "flake8",
8
+ "black",
9
+ "clippy",
10
+ "golangci-lint",
11
+ ];
12
+
13
+ interface Issue {
14
+ severity: "ERROR" | "WARNING";
15
+ rule: string;
16
+ file: string;
17
+ line?: number;
18
+ message: string;
19
+ }
20
+
21
+ export function isLinterCommand(command: string | undefined | null): boolean {
22
+ if (typeof command !== "string" || command.length === 0) {
23
+ return false;
24
+ }
25
+
26
+ const cmdLower = command.toLowerCase();
27
+ return LINTER_COMMANDS.some((lc) => cmdLower.includes(lc));
28
+ }
29
+
30
+ function parseIssues(output: string, linterType: string): Issue[] {
31
+ const issues: Issue[] = [];
32
+ const lines = output.split("\n");
33
+
34
+ for (const line of lines) {
35
+ const issue = parseLine(line, linterType);
36
+ if (issue) {
37
+ issues.push(issue);
38
+ }
39
+ }
40
+
41
+ return issues;
42
+ }
43
+
44
+ function parseLine(line: string, linterType: string): Issue | null {
45
+ // ESLint: /path/to/file.js:10:5: Error message [rule-id]
46
+ // Ruff: /path/to/file.py:10:5: E501 Error message
47
+ // Pylint: /path/to/file.py:10:5: E0001: Error message (rule-id)
48
+ // Clippy: error: message at src/main.rs:10:5
49
+
50
+ const patterns = [
51
+ // file:line:col: message [rule]
52
+ {
53
+ pattern: /^(.+):(\d+):(\d+):\s*(.+)$/,
54
+ extract: (match: RegExpMatchArray) => ({
55
+ file: match[1],
56
+ line: parseInt(match[2], 10),
57
+ content: match[4],
58
+ }),
59
+ },
60
+ // error: message at file:line:col
61
+ {
62
+ pattern: /^(error|warning):\s*(.+?)\s+at\s+(.+):(\d+):(\d+)$/,
63
+ extract: (match: RegExpMatchArray) => ({
64
+ severity: match[1].toUpperCase() as "ERROR" | "WARNING",
65
+ message: match[2],
66
+ file: match[3],
67
+ line: parseInt(match[4], 10),
68
+ content: match[2],
69
+ }),
70
+ },
71
+ ];
72
+
73
+ for (const { pattern, extract } of patterns) {
74
+ const match = line.match(pattern);
75
+ if (match) {
76
+ const extracted = extract(match);
77
+ return {
78
+ severity: extracted.severity || "ERROR",
79
+ rule: extracted.content?.match(/\[(.+?)\]$/)?.[1] || "unknown",
80
+ file: extracted.file,
81
+ line: extracted.line,
82
+ message: extracted.content || extracted.message || line,
83
+ };
84
+ }
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ export function aggregateLinterOutput(
91
+ output: string,
92
+ command: string | undefined | null
93
+ ): string | null {
94
+ if (typeof command !== "string" || !isLinterCommand(command)) {
95
+ return null;
96
+ }
97
+
98
+ // Detect linter type from command
99
+ const linterType = detectLinterType(command);
100
+
101
+ // Parse issues
102
+ const issues = parseIssues(output, linterType);
103
+
104
+ if (issues.length === 0) {
105
+ return `āœ“ ${linterType}: No issues found`;
106
+ }
107
+
108
+ // Count by severity
109
+ const errors = issues.filter((i) => i.severity === "ERROR").length;
110
+ const warnings = issues.filter((i) => i.severity === "WARNING").length;
111
+
112
+ // Group by rule
113
+ const byRule = new Map<string, number>();
114
+ for (const issue of issues) {
115
+ byRule.set(issue.rule, (byRule.get(issue.rule) || 0) + 1);
116
+ }
117
+
118
+ // Group by file
119
+ const byFile = new Map<string, Issue[]>();
120
+ for (const issue of issues) {
121
+ const existing = byFile.get(issue.file) || [];
122
+ existing.push(issue);
123
+ byFile.set(issue.file, existing);
124
+ }
125
+
126
+ // Build output
127
+ let result = `${linterType}: ${errors} errors, ${warnings} warnings in ${byFile.size} files\n`;
128
+ result += "═══════════════════════════════════════\n";
129
+
130
+ // Top rules
131
+ const sortedRules = Array.from(byRule.entries())
132
+ .sort((a, b) => b[1] - a[1])
133
+ .slice(0, 10);
134
+ result += "Top rules:\n";
135
+ for (const [rule, count] of sortedRules) {
136
+ result += ` ${rule} (${count}x)\n`;
137
+ }
138
+
139
+ // Top files
140
+ result += "\nTop files:\n";
141
+ const sortedFiles = Array.from(byFile.entries())
142
+ .sort((a, b) => b[1].length - a[1].length)
143
+ .slice(0, 10);
144
+
145
+ for (const [file, fileIssues] of sortedFiles) {
146
+ const compact = compactPath(file, 40);
147
+ result += ` ${compact} (${fileIssues.length} issues)\n`;
148
+
149
+ // Show top 3 rules per file
150
+ const fileRules = new Map<string, number>();
151
+ for (const issue of fileIssues) {
152
+ fileRules.set(issue.rule, (fileRules.get(issue.rule) || 0) + 1);
153
+ }
154
+
155
+ const sortedFileRules = Array.from(fileRules.entries())
156
+ .sort((a, b) => b[1] - a[1])
157
+ .slice(0, 3);
158
+
159
+ for (const [rule, count] of sortedFileRules) {
160
+ result += ` ${rule} (${count})\n`;
161
+ }
162
+ }
163
+
164
+ return result;
165
+ }
166
+
167
+ function detectLinterType(command: string): string {
168
+ const cmdLower = command.toLowerCase();
169
+ if (cmdLower.includes("eslint")) return "ESLint";
170
+ if (cmdLower.includes("ruff")) return "Ruff";
171
+ if (cmdLower.includes("pylint")) return "Pylint";
172
+ if (cmdLower.includes("mypy")) return "MyPy";
173
+ if (cmdLower.includes("flake8")) return "Flake8";
174
+ if (cmdLower.includes("clippy")) return "Clippy";
175
+ if (cmdLower.includes("golangci")) return "GolangCI-Lint";
176
+ if (cmdLower.includes("prettier")) return "Prettier";
177
+ return "Linter";
178
+ }
179
+
180
+ function compactPath(path: string, maxLength: number): string {
181
+ if (path.length <= maxLength) {
182
+ return path;
183
+ }
184
+
185
+ const parts = path.split("/");
186
+ if (parts.length <= 3) {
187
+ return path;
188
+ }
189
+
190
+ return `${parts[0]}/.../${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
191
+ }
@@ -0,0 +1,100 @@
1
+ const SEARCH_COMMANDS = ["grep", "rg", "find", "ack", "ag"];
2
+
3
+ export function isSearchCommand(command: string | undefined | null): boolean {
4
+ if (typeof command !== "string" || command.length === 0) {
5
+ return false;
6
+ }
7
+
8
+ const cmdLower = command.toLowerCase();
9
+ return SEARCH_COMMANDS.some((sc) => cmdLower.includes(sc));
10
+ }
11
+
12
+ interface SearchResult {
13
+ file: string;
14
+ lineNumber: string;
15
+ content: string;
16
+ }
17
+
18
+ export function groupSearchResults(
19
+ output: string,
20
+ maxResults: number = 50
21
+ ): string | null {
22
+ const lines = output.split("\n");
23
+ const results: SearchResult[] = [];
24
+
25
+ // Parse search results
26
+ for (const line of lines) {
27
+ if (!line.trim()) continue;
28
+
29
+ // Match patterns like: file:line:content or file:content
30
+ const match = line.match(/^(.+?):(\d+)?:(.+)$/);
31
+ if (match) {
32
+ results.push({
33
+ file: match[1],
34
+ lineNumber: match[2] || "?",
35
+ content: match[3],
36
+ });
37
+ }
38
+ }
39
+
40
+ if (results.length === 0) {
41
+ return null;
42
+ }
43
+
44
+ // Group by file
45
+ const byFile = new Map<string, SearchResult[]>();
46
+ for (const result of results) {
47
+ const existing = byFile.get(result.file) || [];
48
+ existing.push(result);
49
+ byFile.set(result.file, existing);
50
+ }
51
+
52
+ // Build output
53
+ let output_text = `šŸ” ${results.length} matches in ${byFile.size} files:\n\n`;
54
+
55
+ const files = Array.from(byFile.entries()).sort((a, b) => a[0].localeCompare(b[0]));
56
+ let shown = 0;
57
+
58
+ for (const [file, matches] of files) {
59
+ if (shown >= maxResults) {
60
+ break;
61
+ }
62
+
63
+ const compactFile = compactPath(file, 50);
64
+ output_text += `šŸ“„ ${compactFile} (${matches.length} matches):\n`;
65
+
66
+ for (const match of matches.slice(0, 10)) {
67
+ let cleaned = match.content.trim();
68
+ if (cleaned.length > 70) {
69
+ cleaned = cleaned.slice(0, 67) + "...";
70
+ }
71
+ output_text += ` ${match.lineNumber}: ${cleaned}\n`;
72
+ shown++;
73
+ }
74
+
75
+ if (matches.length > 10) {
76
+ output_text += ` +${matches.length - 10} more\n`;
77
+ }
78
+
79
+ output_text += "\n";
80
+ }
81
+
82
+ if (results.length > shown) {
83
+ output_text += `... +${results.length - shown} more\n`;
84
+ }
85
+
86
+ return output_text;
87
+ }
88
+
89
+ function compactPath(path: string, maxLength: number): string {
90
+ if (path.length <= maxLength) {
91
+ return path;
92
+ }
93
+
94
+ const parts = path.split("/");
95
+ if (parts.length <= 3) {
96
+ return path;
97
+ }
98
+
99
+ return `${parts[0]}/.../${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
100
+ }