@hasna/terminal 2.0.5 → 2.2.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/dist/cli.js +23 -9
- package/package.json +1 -1
- package/src/ai.ts +46 -113
- package/src/cli.tsx +22 -9
- package/src/command-validator.ts +11 -0
- package/src/context-hints.ts +202 -0
- package/src/output-processor.ts +7 -18
- package/src/providers/base.ts +3 -1
- package/src/providers/groq.ts +108 -0
- package/src/providers/index.ts +26 -2
- package/src/providers/providers.test.ts +4 -2
- package/src/providers/xai.ts +108 -0
- package/dist/App.js +0 -404
- package/dist/Browse.js +0 -79
- package/dist/FuzzyPicker.js +0 -47
- package/dist/Onboarding.js +0 -51
- package/dist/Spinner.js +0 -12
- package/dist/StatusBar.js +0 -49
- package/dist/ai.js +0 -368
- package/dist/cache.js +0 -41
- package/dist/command-rewriter.js +0 -64
- package/dist/command-validator.js +0 -77
- package/dist/compression.js +0 -107
- package/dist/diff-cache.js +0 -107
- package/dist/economy.js +0 -79
- package/dist/expand-store.js +0 -38
- package/dist/file-cache.js +0 -72
- package/dist/file-index.js +0 -62
- package/dist/history.js +0 -62
- package/dist/lazy-executor.js +0 -54
- package/dist/line-dedup.js +0 -59
- package/dist/loop-detector.js +0 -75
- package/dist/mcp/install.js +0 -98
- package/dist/mcp/server.js +0 -569
- package/dist/noise-filter.js +0 -86
- package/dist/output-processor.js +0 -136
- package/dist/output-router.js +0 -41
- package/dist/parsers/base.js +0 -2
- package/dist/parsers/build.js +0 -64
- package/dist/parsers/errors.js +0 -101
- package/dist/parsers/files.js +0 -78
- package/dist/parsers/git.js +0 -99
- package/dist/parsers/index.js +0 -48
- package/dist/parsers/tests.js +0 -89
- package/dist/providers/anthropic.js +0 -39
- package/dist/providers/base.js +0 -4
- package/dist/providers/cerebras.js +0 -95
- package/dist/providers/index.js +0 -49
- package/dist/recipes/model.js +0 -20
- package/dist/recipes/storage.js +0 -136
- package/dist/search/content-search.js +0 -68
- package/dist/search/file-search.js +0 -61
- package/dist/search/filters.js +0 -34
- package/dist/search/index.js +0 -5
- package/dist/search/semantic.js +0 -320
- package/dist/session-boot.js +0 -59
- package/dist/session-context.js +0 -55
- package/dist/sessions-db.js +0 -120
- package/dist/smart-display.js +0 -286
- package/dist/snapshots.js +0 -51
- package/dist/supervisor.js +0 -112
- package/dist/test-watchlist.js +0 -131
- package/dist/tree.js +0 -94
- package/dist/usage-cache.js +0 -65
package/dist/output-processor.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// AI-powered output processor — uses cheap AI to intelligently summarize any output
|
|
2
|
-
// NOTHING is hardcoded. The AI decides what's important, what's noise, what to keep.
|
|
3
|
-
import { getProvider } from "./providers/index.js";
|
|
4
|
-
import { estimateTokens } from "./parsers/index.js";
|
|
5
|
-
import { recordSaving } from "./economy.js";
|
|
6
|
-
const MIN_LINES_TO_PROCESS = 15;
|
|
7
|
-
const MAX_OUTPUT_FOR_AI = 8000; // chars to send to AI (truncate if longer)
|
|
8
|
-
const SUMMARIZE_PROMPT = `You are an intelligent terminal assistant. Given a user's original question and the command output, ANSWER THE QUESTION directly.
|
|
9
|
-
|
|
10
|
-
RULES:
|
|
11
|
-
- If the user asked a YES/NO question, start with Yes or No, then explain briefly
|
|
12
|
-
- If the user asked "how many", give the number first, then context
|
|
13
|
-
- If the user asked "show me X", show only X, not everything
|
|
14
|
-
- ANSWER the question using the data — don't just summarize the raw output
|
|
15
|
-
- Use symbols: ✓ for success/yes, ✗ for failure/no, ⚠ for warnings
|
|
16
|
-
- Maximum 8 lines
|
|
17
|
-
- Keep errors/failures verbatim
|
|
18
|
-
- Be direct and concise — the user wants an ANSWER, not a data dump
|
|
19
|
-
- For TEST OUTPUT: look for "X pass" and "X fail" lines. These are DEFINITIVE. If you see "42 pass, 0 fail" in the output, the answer is "42 tests pass, 0 fail." NEVER say "no tests found" or "incomplete" when pass/fail counts are visible.
|
|
20
|
-
- For BUILD OUTPUT: if tsc/build exits 0 with no output, it SUCCEEDED. Empty output = success.`;
|
|
21
|
-
/**
|
|
22
|
-
* Process command output through AI summarization.
|
|
23
|
-
* Cheap AI call (~100 tokens) saves 1000+ tokens downstream.
|
|
24
|
-
*/
|
|
25
|
-
export async function processOutput(command, output, originalPrompt) {
|
|
26
|
-
const lines = output.split("\n");
|
|
27
|
-
// Short output — skip AI UNLESS we have an original prompt (NL mode needs answer framing)
|
|
28
|
-
if (lines.length <= MIN_LINES_TO_PROCESS && !originalPrompt) {
|
|
29
|
-
return {
|
|
30
|
-
summary: output,
|
|
31
|
-
full: output,
|
|
32
|
-
tokensSaved: 0,
|
|
33
|
-
aiTokensUsed: 0,
|
|
34
|
-
aiProcessed: false,
|
|
35
|
-
aiCostUsd: 0,
|
|
36
|
-
savingsValueUsd: 0,
|
|
37
|
-
netSavingsUsd: 0,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
// Truncate very long output before sending to AI
|
|
41
|
-
let toSummarize = output;
|
|
42
|
-
if (toSummarize.length > MAX_OUTPUT_FOR_AI) {
|
|
43
|
-
const headChars = Math.floor(MAX_OUTPUT_FOR_AI * 0.6);
|
|
44
|
-
const tailChars = Math.floor(MAX_OUTPUT_FOR_AI * 0.3);
|
|
45
|
-
toSummarize = output.slice(0, headChars) +
|
|
46
|
-
`\n\n... (${lines.length} total lines, middle truncated) ...\n\n` +
|
|
47
|
-
output.slice(-tailChars);
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
// Pre-parse: if output contains clear pass/fail counts, extract and return directly
|
|
51
|
-
// No hardcoded test runner list — works for ANY tool that outputs "X pass, Y fail"
|
|
52
|
-
const passMatch = output.match(/(\d+)\s+pass/i);
|
|
53
|
-
const failMatch = output.match(/(\d+)\s+fail/i);
|
|
54
|
-
// Pre-parse fires when output has BOTH pass+fail counts AND the user asked about tests
|
|
55
|
-
if (passMatch && failMatch && originalPrompt && /test|pass|fail/i.test(originalPrompt)) {
|
|
56
|
-
const passed = parseInt(passMatch[1]);
|
|
57
|
-
const failed = parseInt(failMatch[1]);
|
|
58
|
-
const answer = failed === 0
|
|
59
|
-
? `✓ Yes, all ${passed} tests pass.`
|
|
60
|
-
: `✗ ${failed} of ${passed + failed} tests failed.`;
|
|
61
|
-
const savedTokens = estimateTokens(output) - estimateTokens(answer);
|
|
62
|
-
return {
|
|
63
|
-
summary: answer, full: output, tokensSaved: Math.max(0, savedTokens),
|
|
64
|
-
aiTokensUsed: 0, aiProcessed: true, aiCostUsd: 0, savingsValueUsd: 0, netSavingsUsd: 0,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
const provider = getProvider();
|
|
68
|
-
const summary = await provider.complete(`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}`, {
|
|
69
|
-
system: SUMMARIZE_PROMPT,
|
|
70
|
-
maxTokens: 300,
|
|
71
|
-
});
|
|
72
|
-
const originalTokens = estimateTokens(output);
|
|
73
|
-
const summaryTokens = estimateTokens(summary);
|
|
74
|
-
const saved = Math.max(0, originalTokens - summaryTokens);
|
|
75
|
-
if (saved > 0) {
|
|
76
|
-
recordSaving("compressed", saved);
|
|
77
|
-
}
|
|
78
|
-
// Try to extract structured JSON if the AI returned it
|
|
79
|
-
let structured;
|
|
80
|
-
try {
|
|
81
|
-
const jsonMatch = summary.match(/\{[\s\S]*\}/);
|
|
82
|
-
if (jsonMatch) {
|
|
83
|
-
structured = JSON.parse(jsonMatch[0]);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch { /* not JSON, that's fine */ }
|
|
87
|
-
// Cost calculation
|
|
88
|
-
// AI input: system prompt (~200 tokens) + command + output sent to AI
|
|
89
|
-
const aiInputTokens = estimateTokens(SUMMARIZE_PROMPT) + estimateTokens(toSummarize) + 20;
|
|
90
|
-
const aiOutputTokens = summaryTokens;
|
|
91
|
-
const aiTokensUsed = aiInputTokens + aiOutputTokens;
|
|
92
|
-
// Cerebras qwen-3-235b pricing: $0.60/M input, $1.20/M output
|
|
93
|
-
const aiCostUsd = (aiInputTokens * 0.60 + aiOutputTokens * 1.20) / 1_000_000;
|
|
94
|
-
// Value of tokens saved (at Claude Sonnet $3/M input — what the agent would pay)
|
|
95
|
-
const savingsValueUsd = (saved * 3.0) / 1_000_000;
|
|
96
|
-
const netSavingsUsd = savingsValueUsd - aiCostUsd;
|
|
97
|
-
// Only record savings if net positive (AI cost < token savings value)
|
|
98
|
-
if (netSavingsUsd > 0 && saved > 0) {
|
|
99
|
-
recordSaving("compressed", saved);
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
summary,
|
|
103
|
-
full: output,
|
|
104
|
-
structured,
|
|
105
|
-
tokensSaved: saved,
|
|
106
|
-
aiTokensUsed,
|
|
107
|
-
aiProcessed: true,
|
|
108
|
-
aiCostUsd,
|
|
109
|
-
savingsValueUsd,
|
|
110
|
-
netSavingsUsd,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// AI unavailable — fall back to simple truncation
|
|
115
|
-
const head = lines.slice(0, 5).join("\n");
|
|
116
|
-
const tail = lines.slice(-5).join("\n");
|
|
117
|
-
const fallback = `${head}\n ... (${lines.length - 10} lines hidden) ...\n${tail}`;
|
|
118
|
-
return {
|
|
119
|
-
summary: fallback,
|
|
120
|
-
full: output,
|
|
121
|
-
tokensSaved: Math.max(0, estimateTokens(output) - estimateTokens(fallback)),
|
|
122
|
-
aiTokensUsed: 0,
|
|
123
|
-
aiProcessed: false,
|
|
124
|
-
aiCostUsd: 0,
|
|
125
|
-
savingsValueUsd: 0,
|
|
126
|
-
netSavingsUsd: 0,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Lightweight version — just decides IF output should be processed.
|
|
132
|
-
* Returns true if the output would benefit from AI summarization.
|
|
133
|
-
*/
|
|
134
|
-
export function shouldProcess(output) {
|
|
135
|
-
return output.split("\n").length > MIN_LINES_TO_PROCESS;
|
|
136
|
-
}
|
package/dist/output-router.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// Output intelligence router — auto-detect command type and optimize output
|
|
2
|
-
import { parseOutput, estimateTokens } from "./parsers/index.js";
|
|
3
|
-
import { compress, stripAnsi } from "./compression.js";
|
|
4
|
-
import { recordSaving } from "./economy.js";
|
|
5
|
-
/** Route command output through the best optimization path */
|
|
6
|
-
export function routeOutput(command, output, maxTokens) {
|
|
7
|
-
const clean = stripAnsi(output);
|
|
8
|
-
const rawTokens = estimateTokens(clean);
|
|
9
|
-
// Try structured parsing first
|
|
10
|
-
const parsed = parseOutput(command, clean);
|
|
11
|
-
if (parsed) {
|
|
12
|
-
const json = JSON.stringify(parsed.data);
|
|
13
|
-
const jsonTokens = estimateTokens(json);
|
|
14
|
-
const saved = rawTokens - jsonTokens;
|
|
15
|
-
if (saved > 0) {
|
|
16
|
-
recordSaving("structured", saved);
|
|
17
|
-
return {
|
|
18
|
-
raw: clean,
|
|
19
|
-
structured: parsed.data,
|
|
20
|
-
parser: parsed.parser,
|
|
21
|
-
tokensSaved: saved,
|
|
22
|
-
format: "json",
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
// Try compression if structured didn't save enough
|
|
27
|
-
if (maxTokens || rawTokens > 200) {
|
|
28
|
-
const compressed = compress(command, clean, { maxTokens, format: "text" });
|
|
29
|
-
if (compressed.tokensSaved > 0) {
|
|
30
|
-
recordSaving("compressed", compressed.tokensSaved);
|
|
31
|
-
return {
|
|
32
|
-
raw: clean,
|
|
33
|
-
compressed: compressed.content,
|
|
34
|
-
tokensSaved: compressed.tokensSaved,
|
|
35
|
-
format: "compressed",
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
// Return raw if no optimization helps
|
|
40
|
-
return { raw: clean, tokensSaved: 0, format: "raw" };
|
|
41
|
-
}
|
package/dist/parsers/base.js
DELETED
package/dist/parsers/build.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// Parser for build output (npm/bun/pnpm build, tsc, webpack, vite, etc.)
|
|
2
|
-
export const buildParser = {
|
|
3
|
-
name: "build",
|
|
4
|
-
detect(command, output) {
|
|
5
|
-
if (/\b(npm|bun|pnpm|yarn)\s+(run\s+)?build\b/.test(command))
|
|
6
|
-
return true;
|
|
7
|
-
if (/\btsc\b/.test(command))
|
|
8
|
-
return true;
|
|
9
|
-
if (/\b(webpack|vite|esbuild|rollup|turbo)\b/.test(command))
|
|
10
|
-
return true;
|
|
11
|
-
return /\b(compiled|bundled|built)\b/i.test(output) && /\b(success|error|warning)\b/i.test(output);
|
|
12
|
-
},
|
|
13
|
-
parse(_command, output) {
|
|
14
|
-
const lines = output.split("\n");
|
|
15
|
-
let warnings = 0, errors = 0, duration;
|
|
16
|
-
// Count warnings and errors
|
|
17
|
-
for (const line of lines) {
|
|
18
|
-
if (/\bwarning\b/i.test(line))
|
|
19
|
-
warnings++;
|
|
20
|
-
if (/\berror\b/i.test(line) && !/0 errors/.test(line))
|
|
21
|
-
errors++;
|
|
22
|
-
}
|
|
23
|
-
// Specific patterns
|
|
24
|
-
const tscErrors = output.match(/Found (\d+) error/);
|
|
25
|
-
if (tscErrors)
|
|
26
|
-
errors = parseInt(tscErrors[1]);
|
|
27
|
-
const warningCount = output.match(/(\d+)\s+warning/);
|
|
28
|
-
if (warningCount)
|
|
29
|
-
warnings = parseInt(warningCount[1]);
|
|
30
|
-
// Duration
|
|
31
|
-
const timeMatch = output.match(/(?:in|took)\s+([\d.]+\s*(?:s|ms|m))/i) ||
|
|
32
|
-
output.match(/Done in ([\d.]+s)/);
|
|
33
|
-
if (timeMatch)
|
|
34
|
-
duration = timeMatch[1];
|
|
35
|
-
const status = errors > 0 ? "failure" : "success";
|
|
36
|
-
return { status, warnings, errors, duration };
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
export const npmInstallParser = {
|
|
40
|
-
name: "npm-install",
|
|
41
|
-
detect(command, _output) {
|
|
42
|
-
return /\b(npm|bun|pnpm|yarn)\s+(install|add|i)\b/.test(command);
|
|
43
|
-
},
|
|
44
|
-
parse(_command, output) {
|
|
45
|
-
let installed = 0, vulnerabilities = 0, duration;
|
|
46
|
-
// npm: added 47 packages in 3.2s
|
|
47
|
-
const npmMatch = output.match(/added\s+(\d+)\s+packages?\s+in\s+([\d.]+s)/);
|
|
48
|
-
if (npmMatch) {
|
|
49
|
-
installed = parseInt(npmMatch[1]);
|
|
50
|
-
duration = npmMatch[2];
|
|
51
|
-
}
|
|
52
|
-
// bun: 47 packages installed [1.2s]
|
|
53
|
-
const bunMatch = output.match(/(\d+)\s+packages?\s+installed.*?\[([\d.]+[ms]*s)\]/);
|
|
54
|
-
if (!npmMatch && bunMatch) {
|
|
55
|
-
installed = parseInt(bunMatch[1]);
|
|
56
|
-
duration = bunMatch[2];
|
|
57
|
-
}
|
|
58
|
-
// Vulnerabilities
|
|
59
|
-
const vulnMatch = output.match(/(\d+)\s+vulnerabilit/);
|
|
60
|
-
if (vulnMatch)
|
|
61
|
-
vulnerabilities = parseInt(vulnMatch[1]);
|
|
62
|
-
return { installed, vulnerabilities, duration };
|
|
63
|
-
},
|
|
64
|
-
};
|
package/dist/parsers/errors.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// Parser for common error patterns
|
|
2
|
-
const ERROR_PATTERNS = [
|
|
3
|
-
{
|
|
4
|
-
type: "port_in_use",
|
|
5
|
-
pattern: /EADDRINUSE.*?(?::(\d+))|port\s+(\d+)\s+(?:is\s+)?(?:already\s+)?in\s+use/i,
|
|
6
|
-
extract: (m) => ({
|
|
7
|
-
type: "port_in_use",
|
|
8
|
-
message: m[0],
|
|
9
|
-
suggestion: `Kill the process: lsof -i :${m[1] ?? m[2]} -t | xargs kill`,
|
|
10
|
-
}),
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
type: "file_not_found",
|
|
14
|
-
pattern: /ENOENT.*?'([^']+)'|No such file or directory:\s*(.+)/,
|
|
15
|
-
extract: (m) => ({
|
|
16
|
-
type: "file_not_found",
|
|
17
|
-
message: m[0],
|
|
18
|
-
file: m[1] ?? m[2]?.trim(),
|
|
19
|
-
suggestion: "Check the file path exists",
|
|
20
|
-
}),
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
type: "permission_denied",
|
|
24
|
-
pattern: /EACCES.*?'([^']+)'|Permission denied:\s*(.+)/,
|
|
25
|
-
extract: (m) => ({
|
|
26
|
-
type: "permission_denied",
|
|
27
|
-
message: m[0],
|
|
28
|
-
file: m[1] ?? m[2]?.trim(),
|
|
29
|
-
suggestion: "Check file permissions or run with sudo",
|
|
30
|
-
}),
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
type: "command_not_found",
|
|
34
|
-
pattern: /command not found:\s*(\S+)|(\S+):\s*not found/,
|
|
35
|
-
extract: (m) => ({
|
|
36
|
-
type: "command_not_found",
|
|
37
|
-
message: m[0],
|
|
38
|
-
suggestion: `Install ${m[1] ?? m[2]} or check your PATH`,
|
|
39
|
-
}),
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
type: "dependency_missing",
|
|
43
|
-
pattern: /Cannot find module\s+'([^']+)'|Module not found.*?'([^']+)'/,
|
|
44
|
-
extract: (m) => ({
|
|
45
|
-
type: "dependency_missing",
|
|
46
|
-
message: m[0],
|
|
47
|
-
suggestion: `Install: npm install ${m[1] ?? m[2]}`,
|
|
48
|
-
}),
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
type: "syntax_error",
|
|
52
|
-
pattern: /SyntaxError:\s*(.+)|error TS\d+:\s*(.+)/,
|
|
53
|
-
extract: (m, output) => {
|
|
54
|
-
const fileMatch = output.match(/(\S+\.\w+):(\d+)/);
|
|
55
|
-
return {
|
|
56
|
-
type: "syntax_error",
|
|
57
|
-
message: m[1] ?? m[2] ?? m[0],
|
|
58
|
-
file: fileMatch?.[1],
|
|
59
|
-
line: fileMatch ? parseInt(fileMatch[2]) : undefined,
|
|
60
|
-
suggestion: "Fix the syntax error in the referenced file",
|
|
61
|
-
};
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
type: "out_of_memory",
|
|
66
|
-
pattern: /ENOMEM|JavaScript heap out of memory|Killed/,
|
|
67
|
-
extract: (m) => ({
|
|
68
|
-
type: "out_of_memory",
|
|
69
|
-
message: m[0],
|
|
70
|
-
suggestion: "Increase memory: NODE_OPTIONS=--max-old-space-size=4096",
|
|
71
|
-
}),
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
type: "network_error",
|
|
75
|
-
pattern: /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|fetch failed/,
|
|
76
|
-
extract: (m) => ({
|
|
77
|
-
type: "network_error",
|
|
78
|
-
message: m[0],
|
|
79
|
-
suggestion: "Check network connection and target URL/host",
|
|
80
|
-
}),
|
|
81
|
-
},
|
|
82
|
-
];
|
|
83
|
-
export const errorParser = {
|
|
84
|
-
name: "error",
|
|
85
|
-
detect(_command, output) {
|
|
86
|
-
return ERROR_PATTERNS.some(({ pattern }) => pattern.test(output));
|
|
87
|
-
},
|
|
88
|
-
parse(_command, output) {
|
|
89
|
-
for (const { pattern, extract } of ERROR_PATTERNS) {
|
|
90
|
-
const match = output.match(pattern);
|
|
91
|
-
if (match)
|
|
92
|
-
return extract(match, output);
|
|
93
|
-
}
|
|
94
|
-
// Generic error fallback
|
|
95
|
-
const errorLine = output.split("\n").find(l => /error/i.test(l));
|
|
96
|
-
return {
|
|
97
|
-
type: "unknown",
|
|
98
|
-
message: errorLine?.trim() ?? "Unknown error",
|
|
99
|
-
};
|
|
100
|
-
},
|
|
101
|
-
};
|
package/dist/parsers/files.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// Parser for file listing output (ls -la, find, etc.)
|
|
2
|
-
const NODE_MODULES_RE = /node_modules/;
|
|
3
|
-
const DIST_RE = /\b(dist|build|\.next|__pycache__|coverage|\.git)\b/;
|
|
4
|
-
const SOURCE_EXTS = /\.(ts|tsx|js|jsx|py|go|rs|java|rb|sh|c|cpp|h|css|scss|html|vue|svelte|md|json|yaml|yml|toml)$/;
|
|
5
|
-
export const lsParser = {
|
|
6
|
-
name: "ls",
|
|
7
|
-
detect(command, output) {
|
|
8
|
-
return /^\s*(ls|ll|la)\b/.test(command) && output.includes(" ");
|
|
9
|
-
},
|
|
10
|
-
parse(_command, output) {
|
|
11
|
-
const lines = output.split("\n").filter(l => l.trim());
|
|
12
|
-
const entries = [];
|
|
13
|
-
for (const line of lines) {
|
|
14
|
-
// ls -la format: drwxr-xr-x 5 user group 160 Mar 10 09:00 dirname
|
|
15
|
-
const match = line.match(/^([dlcbps-])([rwxsStT-]{9})\s+\d+\s+\S+\s+\S+\s+(\d+)\s+(\w+\s+\d+\s+[\d:]+)\s+(.+)$/);
|
|
16
|
-
if (match) {
|
|
17
|
-
const typeChar = match[1];
|
|
18
|
-
entries.push({
|
|
19
|
-
name: match[5],
|
|
20
|
-
type: typeChar === "d" ? "dir" : typeChar === "l" ? "symlink" : "file",
|
|
21
|
-
size: parseInt(match[3]),
|
|
22
|
-
modified: match[4],
|
|
23
|
-
permissions: match[1] + match[2],
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
else if (line.trim() && !line.startsWith("total ")) {
|
|
27
|
-
// Simple ls output — just filenames
|
|
28
|
-
entries.push({ name: line.trim(), type: "file" });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return entries;
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
export const findParser = {
|
|
35
|
-
name: "find",
|
|
36
|
-
detect(command, _output) {
|
|
37
|
-
return /^\s*(find|fd)\b/.test(command);
|
|
38
|
-
},
|
|
39
|
-
parse(_command, output) {
|
|
40
|
-
const lines = output.split("\n").filter(l => l.trim());
|
|
41
|
-
const source = [];
|
|
42
|
-
const other = [];
|
|
43
|
-
let nodeModulesCount = 0;
|
|
44
|
-
let distCount = 0;
|
|
45
|
-
for (const line of lines) {
|
|
46
|
-
const path = line.trim();
|
|
47
|
-
if (!path)
|
|
48
|
-
continue;
|
|
49
|
-
if (NODE_MODULES_RE.test(path)) {
|
|
50
|
-
nodeModulesCount++;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (DIST_RE.test(path)) {
|
|
54
|
-
distCount++;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const name = path.split("/").pop() ?? path;
|
|
58
|
-
const entry = { name: path, type: SOURCE_EXTS.test(name) ? "file" : "other" };
|
|
59
|
-
if (SOURCE_EXTS.test(name)) {
|
|
60
|
-
source.push(entry);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
other.push(entry);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const filtered = [];
|
|
67
|
-
if (nodeModulesCount > 0)
|
|
68
|
-
filtered.push({ count: nodeModulesCount, reason: "node_modules" });
|
|
69
|
-
if (distCount > 0)
|
|
70
|
-
filtered.push({ count: distCount, reason: "dist/build" });
|
|
71
|
-
return {
|
|
72
|
-
total: lines.length,
|
|
73
|
-
source,
|
|
74
|
-
other,
|
|
75
|
-
filtered,
|
|
76
|
-
};
|
|
77
|
-
},
|
|
78
|
-
};
|
package/dist/parsers/git.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
// Parsers for git output (log, status, diff)
|
|
2
|
-
export const gitLogParser = {
|
|
3
|
-
name: "git-log",
|
|
4
|
-
detect(command, _output) {
|
|
5
|
-
return /\bgit\s+log\b/.test(command);
|
|
6
|
-
},
|
|
7
|
-
parse(_command, output) {
|
|
8
|
-
const entries = [];
|
|
9
|
-
const lines = output.split("\n");
|
|
10
|
-
// Detect oneline format: "abc1234 commit message"
|
|
11
|
-
const firstLine = lines[0]?.trim() ?? "";
|
|
12
|
-
const isOneline = /^[a-f0-9]{7,12}\s+/.test(firstLine) && !firstLine.startsWith("commit ");
|
|
13
|
-
if (isOneline) {
|
|
14
|
-
for (const line of lines) {
|
|
15
|
-
const match = line.trim().match(/^([a-f0-9]{7,12})\s+(.+)$/);
|
|
16
|
-
if (match) {
|
|
17
|
-
entries.push({ hash: match[1], author: "", date: "", message: match[2] });
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return entries;
|
|
21
|
-
}
|
|
22
|
-
// Verbose format
|
|
23
|
-
let hash = "", author = "", date = "", message = [];
|
|
24
|
-
for (const line of lines) {
|
|
25
|
-
const commitMatch = line.match(/^commit\s+([a-f0-9]+)/);
|
|
26
|
-
if (commitMatch) {
|
|
27
|
-
if (hash) {
|
|
28
|
-
entries.push({ hash: hash.slice(0, 8), author, date, message: message.join(" ").trim() });
|
|
29
|
-
}
|
|
30
|
-
hash = commitMatch[1];
|
|
31
|
-
author = "";
|
|
32
|
-
date = "";
|
|
33
|
-
message = [];
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
const authorMatch = line.match(/^Author:\s+(.+)/);
|
|
37
|
-
if (authorMatch) {
|
|
38
|
-
author = authorMatch[1];
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
const dateMatch = line.match(/^Date:\s+(.+)/);
|
|
42
|
-
if (dateMatch) {
|
|
43
|
-
date = dateMatch[1].trim();
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (line.startsWith(" ")) {
|
|
47
|
-
message.push(line.trim());
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (hash) {
|
|
51
|
-
entries.push({ hash: hash.slice(0, 8), author, date, message: message.join(" ").trim() });
|
|
52
|
-
}
|
|
53
|
-
return entries;
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
export const gitStatusParser = {
|
|
57
|
-
name: "git-status",
|
|
58
|
-
detect(command, _output) {
|
|
59
|
-
return /\bgit\s+status\b/.test(command);
|
|
60
|
-
},
|
|
61
|
-
parse(_command, output) {
|
|
62
|
-
const lines = output.split("\n");
|
|
63
|
-
let branch = "";
|
|
64
|
-
const staged = [];
|
|
65
|
-
const unstaged = [];
|
|
66
|
-
const untracked = [];
|
|
67
|
-
const branchMatch = output.match(/On branch\s+(\S+)/);
|
|
68
|
-
if (branchMatch)
|
|
69
|
-
branch = branchMatch[1];
|
|
70
|
-
let section = "";
|
|
71
|
-
for (const line of lines) {
|
|
72
|
-
if (line.includes("Changes to be committed")) {
|
|
73
|
-
section = "staged";
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (line.includes("Changes not staged")) {
|
|
77
|
-
section = "unstaged";
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (line.includes("Untracked files")) {
|
|
81
|
-
section = "untracked";
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
const fileMatch = line.match(/^\s+(?:new file|modified|deleted|renamed):\s+(.+)/);
|
|
85
|
-
if (fileMatch) {
|
|
86
|
-
if (section === "staged")
|
|
87
|
-
staged.push(fileMatch[1].trim());
|
|
88
|
-
else if (section === "unstaged")
|
|
89
|
-
unstaged.push(fileMatch[1].trim());
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
// Untracked files are just indented filenames
|
|
93
|
-
if (section === "untracked" && line.match(/^\s+\S/) && !line.includes("(use ")) {
|
|
94
|
-
untracked.push(line.trim());
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return { branch, staged, unstaged, untracked };
|
|
98
|
-
},
|
|
99
|
-
};
|
package/dist/parsers/index.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Output parser registry — auto-detect command output type and parse to structured JSON
|
|
2
|
-
import { lsParser, findParser } from "./files.js";
|
|
3
|
-
import { testParser } from "./tests.js";
|
|
4
|
-
import { gitLogParser, gitStatusParser } from "./git.js";
|
|
5
|
-
import { buildParser, npmInstallParser } from "./build.js";
|
|
6
|
-
import { errorParser } from "./errors.js";
|
|
7
|
-
// Ordered by specificity — more specific parsers first
|
|
8
|
-
const parsers = [
|
|
9
|
-
npmInstallParser,
|
|
10
|
-
testParser,
|
|
11
|
-
gitLogParser,
|
|
12
|
-
gitStatusParser,
|
|
13
|
-
buildParser,
|
|
14
|
-
findParser,
|
|
15
|
-
lsParser,
|
|
16
|
-
errorParser, // fallback for error detection
|
|
17
|
-
];
|
|
18
|
-
/** Try to parse command output with the best matching parser */
|
|
19
|
-
export function parseOutput(command, output) {
|
|
20
|
-
for (const parser of parsers) {
|
|
21
|
-
if (parser.detect(command, output)) {
|
|
22
|
-
try {
|
|
23
|
-
const data = parser.parse(command, output);
|
|
24
|
-
return { parser: parser.name, data, raw: output };
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
/** Get all parsers that match (for debugging/info) */
|
|
34
|
-
export function detectParsers(command, output) {
|
|
35
|
-
return parsers.filter(p => p.detect(command, output)).map(p => p.name);
|
|
36
|
-
}
|
|
37
|
-
/** Estimate token count for a string (rough: ~4 chars per token) */
|
|
38
|
-
export function estimateTokens(text) {
|
|
39
|
-
return Math.ceil(text.length / 4);
|
|
40
|
-
}
|
|
41
|
-
/** Calculate token savings between raw output and parsed JSON */
|
|
42
|
-
export function tokenSavings(raw, parsed) {
|
|
43
|
-
const rawTokens = estimateTokens(raw);
|
|
44
|
-
const parsedTokens = estimateTokens(JSON.stringify(parsed));
|
|
45
|
-
const saved = Math.max(0, rawTokens - parsedTokens);
|
|
46
|
-
const percent = rawTokens > 0 ? Math.round((saved / rawTokens) * 100) : 0;
|
|
47
|
-
return { rawTokens, parsedTokens, saved, percent };
|
|
48
|
-
}
|
package/dist/parsers/tests.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// Parser for test runner output (jest, vitest, bun test, pytest, go test)
|
|
2
|
-
export const testParser = {
|
|
3
|
-
name: "test",
|
|
4
|
-
detect(command, output) {
|
|
5
|
-
if (/\b(jest|vitest|bun\s+test|pytest|go\s+test|mocha|ava|tap)\b/.test(command))
|
|
6
|
-
return true;
|
|
7
|
-
if (/\b(npm|bun|pnpm|yarn)\s+(run\s+)?test\b/.test(command))
|
|
8
|
-
return true;
|
|
9
|
-
// Detect by output patterns
|
|
10
|
-
return /Tests:\s+\d+/.test(output) || /\d+\s+(passing|passed|failed)/.test(output) || /PASS|FAIL/.test(output);
|
|
11
|
-
},
|
|
12
|
-
parse(_command, output) {
|
|
13
|
-
const failures = [];
|
|
14
|
-
let passed = 0, failed = 0, skipped = 0, duration;
|
|
15
|
-
// Jest/Vitest style: Tests: 5 passed, 2 failed, 7 total
|
|
16
|
-
const jestMatch = output.match(/Tests:\s+(?:(\d+)\s+passed)?[,\s]*(?:(\d+)\s+failed)?[,\s]*(?:(\d+)\s+skipped)?[,\s]*(\d+)\s+total/);
|
|
17
|
-
if (jestMatch) {
|
|
18
|
-
passed = parseInt(jestMatch[1] ?? "0");
|
|
19
|
-
failed = parseInt(jestMatch[2] ?? "0");
|
|
20
|
-
skipped = parseInt(jestMatch[3] ?? "0");
|
|
21
|
-
}
|
|
22
|
-
// Bun test style: 5 pass, 2 fail
|
|
23
|
-
const bunMatch = output.match(/(\d+)\s+pass.*?(\d+)\s+fail/);
|
|
24
|
-
if (!jestMatch && bunMatch) {
|
|
25
|
-
passed = parseInt(bunMatch[1]);
|
|
26
|
-
failed = parseInt(bunMatch[2]);
|
|
27
|
-
}
|
|
28
|
-
// Pytest style: 5 passed, 2 failed
|
|
29
|
-
const pytestMatch = output.match(/(\d+)\s+passed(?:.*?(\d+)\s+failed)?/);
|
|
30
|
-
if (!jestMatch && !bunMatch && pytestMatch) {
|
|
31
|
-
passed = parseInt(pytestMatch[1]);
|
|
32
|
-
failed = parseInt(pytestMatch[2] ?? "0");
|
|
33
|
-
}
|
|
34
|
-
// Go test: ok/FAIL + count
|
|
35
|
-
const goPassMatch = output.match(/ok\s+\S+\s+([\d.]+s)/);
|
|
36
|
-
const goFailMatch = output.match(/FAIL\s+\S+/);
|
|
37
|
-
if (!jestMatch && !bunMatch && !pytestMatch && (goPassMatch || goFailMatch)) {
|
|
38
|
-
const passLines = (output.match(/--- PASS/g) || []).length;
|
|
39
|
-
const failLines = (output.match(/--- FAIL/g) || []).length;
|
|
40
|
-
passed = passLines;
|
|
41
|
-
failed = failLines;
|
|
42
|
-
if (goPassMatch)
|
|
43
|
-
duration = goPassMatch[1];
|
|
44
|
-
}
|
|
45
|
-
// Duration
|
|
46
|
-
const timeMatch = output.match(/Time:\s+([\d.]+\s*(?:s|ms|m))/i) || output.match(/in\s+([\d.]+\s*(?:s|ms|m))/i);
|
|
47
|
-
if (timeMatch)
|
|
48
|
-
duration = timeMatch[1];
|
|
49
|
-
// Extract failure details: lines starting with FAIL or ✗ or ×
|
|
50
|
-
const lines = output.split("\n");
|
|
51
|
-
let capturingFailure = false;
|
|
52
|
-
let currentTest = "";
|
|
53
|
-
let currentError = [];
|
|
54
|
-
for (const line of lines) {
|
|
55
|
-
const failMatch = line.match(/(?:FAIL|✗|×|✕)\s+(.+)/);
|
|
56
|
-
if (failMatch) {
|
|
57
|
-
if (capturingFailure && currentTest) {
|
|
58
|
-
failures.push({ test: currentTest, error: currentError.join("\n").trim() });
|
|
59
|
-
}
|
|
60
|
-
currentTest = failMatch[1].trim();
|
|
61
|
-
currentError = [];
|
|
62
|
-
capturingFailure = true;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (capturingFailure) {
|
|
66
|
-
if (line.match(/^(PASS|✓|✔|FAIL|✗|×|✕)\s/) || line.match(/^Tests:|^\d+ pass/)) {
|
|
67
|
-
failures.push({ test: currentTest, error: currentError.join("\n").trim() });
|
|
68
|
-
capturingFailure = false;
|
|
69
|
-
currentTest = "";
|
|
70
|
-
currentError = [];
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
currentError.push(line);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (capturingFailure && currentTest) {
|
|
78
|
-
failures.push({ test: currentTest, error: currentError.join("\n").trim() });
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
passed,
|
|
82
|
-
failed,
|
|
83
|
-
skipped,
|
|
84
|
-
total: passed + failed + skipped,
|
|
85
|
-
duration,
|
|
86
|
-
failures,
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
|
-
};
|