@hasna/terminal 3.3.1 → 3.3.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/dist/mcp/server.js +19 -7
- package/dist/noise-filter.js +13 -5
- package/dist/output-processor.js +4 -1
- package/package.json +1 -1
- package/src/mcp/server.ts +21 -7
- package/src/noise-filter.ts +13 -5
- package/src/output-processor.ts +4 -1
package/dist/mcp/server.js
CHANGED
|
@@ -21,9 +21,9 @@ import { shouldBeLazy, toLazy } from "../lazy-executor.js";
|
|
|
21
21
|
import { getEconomyStats, recordSaving } from "../economy.js";
|
|
22
22
|
import { captureSnapshot } from "../snapshots.js";
|
|
23
23
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
24
|
-
function exec(command, cwd, timeout) {
|
|
25
|
-
//
|
|
26
|
-
const rw = rewriteCommand(command);
|
|
24
|
+
function exec(command, cwd, timeout, allowRewrite = false) {
|
|
25
|
+
// Only rewrite when explicitly allowed (execute_smart, not raw execute)
|
|
26
|
+
const rw = allowRewrite ? rewriteCommand(command) : { changed: false, rewritten: command };
|
|
27
27
|
const actualCommand = rw.changed ? rw.rewritten : command;
|
|
28
28
|
return new Promise((resolve) => {
|
|
29
29
|
const start = Date.now();
|
|
@@ -60,7 +60,7 @@ export function createServer() {
|
|
|
60
60
|
version: "0.2.0",
|
|
61
61
|
});
|
|
62
62
|
// ── execute: run a command, return structured result ──────────────────────
|
|
63
|
-
server.tool("execute", "Run a shell command and return
|
|
63
|
+
server.tool("execute", "Run a shell command and return raw output. Prefer execute_smart for most tasks — it AI-summarizes output, saving 80% tokens. Use execute only when you need the full unprocessed output (e.g., to parse it yourself).", {
|
|
64
64
|
command: z.string().describe("Shell command to execute"),
|
|
65
65
|
cwd: z.string().optional().describe("Working directory (default: server cwd)"),
|
|
66
66
|
timeout: z.number().optional().describe("Timeout in ms (default: 30000)"),
|
|
@@ -135,7 +135,7 @@ export function createServer() {
|
|
|
135
135
|
cwd: z.string().optional().describe("Working directory"),
|
|
136
136
|
timeout: z.number().optional().describe("Timeout in ms (default: 30000)"),
|
|
137
137
|
}, async ({ command, cwd, timeout }) => {
|
|
138
|
-
const result = await exec(command, cwd, timeout ?? 30000);
|
|
138
|
+
const result = await exec(command, cwd, timeout ?? 30000, true); // allow rewrite for smart mode
|
|
139
139
|
const output = (result.stdout + result.stderr).trim();
|
|
140
140
|
const processed = await processOutput(command, output);
|
|
141
141
|
// Progressive disclosure: store full output, return summary + expand key
|
|
@@ -447,12 +447,24 @@ export function createServer() {
|
|
|
447
447
|
};
|
|
448
448
|
});
|
|
449
449
|
// ── read_file: cached file reading ─────────────────────────────────────────
|
|
450
|
-
server.tool("read_file", "Read a file with session caching.
|
|
450
|
+
server.tool("read_file", "Read a file with session caching. Use summarize=true to get an AI-generated outline (~90% fewer tokens) instead of full content — ideal when you just want to understand what a file does without reading every line.", {
|
|
451
451
|
path: z.string().describe("File path"),
|
|
452
452
|
offset: z.number().optional().describe("Start line (0-indexed)"),
|
|
453
453
|
limit: z.number().optional().describe("Max lines to return"),
|
|
454
|
-
|
|
454
|
+
summarize: z.boolean().optional().describe("Return AI summary instead of full content (saves ~90% tokens)"),
|
|
455
|
+
}, async ({ path, offset, limit, summarize }) => {
|
|
455
456
|
const result = cachedRead(path, { offset, limit });
|
|
457
|
+
if (summarize && result.content.length > 500) {
|
|
458
|
+
const processed = await processOutput(`cat ${path}`, result.content);
|
|
459
|
+
return {
|
|
460
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
461
|
+
summary: processed.summary,
|
|
462
|
+
lines: result.content.split("\n").length,
|
|
463
|
+
tokensSaved: processed.tokensSaved,
|
|
464
|
+
cached: result.cached,
|
|
465
|
+
}) }],
|
|
466
|
+
};
|
|
467
|
+
}
|
|
456
468
|
return {
|
|
457
469
|
content: [{ type: "text", text: JSON.stringify({
|
|
458
470
|
content: result.content,
|
package/dist/noise-filter.js
CHANGED
|
@@ -33,15 +33,23 @@ const NOISE_PATTERNS = [
|
|
|
33
33
|
// Generic download/upload progress
|
|
34
34
|
/^\s*\d+(\.\d+)?\s*[KMG]?B\s*\/\s*\d+(\.\d+)?\s*[KMG]?B\b/,
|
|
35
35
|
];
|
|
36
|
-
// Sensitive env var patterns —
|
|
36
|
+
// Sensitive env var patterns — ONLY match actual env var assignments (export X=val, X=val at line start)
|
|
37
|
+
// NOT code lines like `const API_KEY = process.env.API_KEY` or `this.token = config.token`
|
|
37
38
|
const SENSITIVE_PATTERNS = [
|
|
38
|
-
|
|
39
|
-
/^(
|
|
39
|
+
// export KEY_NAME="value" or KEY_NAME=value (shell env vars only)
|
|
40
|
+
/^(export\s+[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[A-Z_]*)=(.+)$/,
|
|
41
|
+
// Plain env assignment at start of line (no leading whitespace = not code)
|
|
42
|
+
/^([A-Z_]*(?:API_KEY|ACCESS_KEY|PRIVATE_KEY|CLIENT_SECRET|AUTH_TOKEN)[A-Z_]*)=(.+)$/,
|
|
40
43
|
];
|
|
41
|
-
/** Redact sensitive values in output (env vars,
|
|
44
|
+
/** Redact sensitive values in output (env vars only, not code) */
|
|
42
45
|
function redactSensitive(line) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
// Skip lines that look like code (have leading whitespace, semicolons, const/let/var, etc.)
|
|
48
|
+
if (/^\s*(const|let|var|this\.|private|public|protected|import|export\s+(default|const|let|function|class)|\/\/|\/\*|\*)/.test(line)) {
|
|
49
|
+
return line; // Code — never redact
|
|
50
|
+
}
|
|
43
51
|
for (const pattern of SENSITIVE_PATTERNS) {
|
|
44
|
-
const match =
|
|
52
|
+
const match = trimmed.match(pattern);
|
|
45
53
|
if (match) {
|
|
46
54
|
return `${match[1]}=[REDACTED]`;
|
|
47
55
|
}
|
package/dist/output-processor.js
CHANGED
|
@@ -90,7 +90,10 @@ RULES:
|
|
|
90
90
|
- Keep errors/failures verbatim
|
|
91
91
|
- Be direct and concise — the user wants an ANSWER, not a data dump
|
|
92
92
|
- 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.
|
|
93
|
-
- For BUILD OUTPUT: if tsc/build exits 0 with no output, it SUCCEEDED. Empty output = success
|
|
93
|
+
- For BUILD OUTPUT: if tsc/build exits 0 with no output, it SUCCEEDED. Empty output = success.
|
|
94
|
+
- For GREP/SEARCH OUTPUT (file:line:match format): List ALL matches grouped by file. NEVER summarize into one sentence. Format: "N matches in M files:" then list each match. The agent needs every match, not a prose interpretation.
|
|
95
|
+
- For FILE LISTINGS (ls, find): show count + key entries. "23 files: src/ai.ts, src/cli.tsx, ..."
|
|
96
|
+
- For GIT LOG/DIFF: preserve commit hashes, file names, and +/- line counts.`;
|
|
94
97
|
/**
|
|
95
98
|
* Process command output through AI summarization.
|
|
96
99
|
* Cheap AI call (~100 tokens) saves 1000+ tokens downstream.
|
package/package.json
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -24,9 +24,9 @@ import { captureSnapshot } from "../snapshots.js";
|
|
|
24
24
|
|
|
25
25
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
|
-
function exec(command: string, cwd?: string, timeout?: number): Promise<{ exitCode: number; stdout: string; stderr: string; duration: number; rewritten?: string }> {
|
|
28
|
-
//
|
|
29
|
-
const rw = rewriteCommand(command);
|
|
27
|
+
function exec(command: string, cwd?: string, timeout?: number, allowRewrite: boolean = false): Promise<{ exitCode: number; stdout: string; stderr: string; duration: number; rewritten?: string }> {
|
|
28
|
+
// Only rewrite when explicitly allowed (execute_smart, not raw execute)
|
|
29
|
+
const rw = allowRewrite ? rewriteCommand(command) : { changed: false, rewritten: command };
|
|
30
30
|
const actualCommand = rw.changed ? rw.rewritten : command;
|
|
31
31
|
return new Promise((resolve) => {
|
|
32
32
|
const start = Date.now();
|
|
@@ -69,7 +69,7 @@ export function createServer(): McpServer {
|
|
|
69
69
|
|
|
70
70
|
server.tool(
|
|
71
71
|
"execute",
|
|
72
|
-
"Run a shell command and return
|
|
72
|
+
"Run a shell command and return raw output. Prefer execute_smart for most tasks — it AI-summarizes output, saving 80% tokens. Use execute only when you need the full unprocessed output (e.g., to parse it yourself).",
|
|
73
73
|
{
|
|
74
74
|
command: z.string().describe("Shell command to execute"),
|
|
75
75
|
cwd: z.string().optional().describe("Working directory (default: server cwd)"),
|
|
@@ -156,7 +156,7 @@ export function createServer(): McpServer {
|
|
|
156
156
|
timeout: z.number().optional().describe("Timeout in ms (default: 30000)"),
|
|
157
157
|
},
|
|
158
158
|
async ({ command, cwd, timeout }) => {
|
|
159
|
-
const result = await exec(command, cwd, timeout ?? 30000);
|
|
159
|
+
const result = await exec(command, cwd, timeout ?? 30000, true); // allow rewrite for smart mode
|
|
160
160
|
const output = (result.stdout + result.stderr).trim();
|
|
161
161
|
const processed = await processOutput(command, output);
|
|
162
162
|
|
|
@@ -638,14 +638,28 @@ export function createServer(): McpServer {
|
|
|
638
638
|
|
|
639
639
|
server.tool(
|
|
640
640
|
"read_file",
|
|
641
|
-
"Read a file with session caching.
|
|
641
|
+
"Read a file with session caching. Use summarize=true to get an AI-generated outline (~90% fewer tokens) instead of full content — ideal when you just want to understand what a file does without reading every line.",
|
|
642
642
|
{
|
|
643
643
|
path: z.string().describe("File path"),
|
|
644
644
|
offset: z.number().optional().describe("Start line (0-indexed)"),
|
|
645
645
|
limit: z.number().optional().describe("Max lines to return"),
|
|
646
|
+
summarize: z.boolean().optional().describe("Return AI summary instead of full content (saves ~90% tokens)"),
|
|
646
647
|
},
|
|
647
|
-
async ({ path, offset, limit }) => {
|
|
648
|
+
async ({ path, offset, limit, summarize }) => {
|
|
648
649
|
const result = cachedRead(path, { offset, limit });
|
|
650
|
+
|
|
651
|
+
if (summarize && result.content.length > 500) {
|
|
652
|
+
const processed = await processOutput(`cat ${path}`, result.content);
|
|
653
|
+
return {
|
|
654
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
655
|
+
summary: processed.summary,
|
|
656
|
+
lines: result.content.split("\n").length,
|
|
657
|
+
tokensSaved: processed.tokensSaved,
|
|
658
|
+
cached: result.cached,
|
|
659
|
+
}) }],
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
649
663
|
return {
|
|
650
664
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
651
665
|
content: result.content,
|
package/src/noise-filter.ts
CHANGED
|
@@ -41,16 +41,24 @@ const NOISE_PATTERNS: RegExp[] = [
|
|
|
41
41
|
/^\s*\d+(\.\d+)?\s*[KMG]?B\s*\/\s*\d+(\.\d+)?\s*[KMG]?B\b/,
|
|
42
42
|
];
|
|
43
43
|
|
|
44
|
-
// Sensitive env var patterns —
|
|
44
|
+
// Sensitive env var patterns — ONLY match actual env var assignments (export X=val, X=val at line start)
|
|
45
|
+
// NOT code lines like `const API_KEY = process.env.API_KEY` or `this.token = config.token`
|
|
45
46
|
const SENSITIVE_PATTERNS = [
|
|
46
|
-
|
|
47
|
-
/^(
|
|
47
|
+
// export KEY_NAME="value" or KEY_NAME=value (shell env vars only)
|
|
48
|
+
/^(export\s+[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[A-Z_]*)=(.+)$/,
|
|
49
|
+
// Plain env assignment at start of line (no leading whitespace = not code)
|
|
50
|
+
/^([A-Z_]*(?:API_KEY|ACCESS_KEY|PRIVATE_KEY|CLIENT_SECRET|AUTH_TOKEN)[A-Z_]*)=(.+)$/,
|
|
48
51
|
];
|
|
49
52
|
|
|
50
|
-
/** Redact sensitive values in output (env vars,
|
|
53
|
+
/** Redact sensitive values in output (env vars only, not code) */
|
|
51
54
|
function redactSensitive(line: string): string {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
// Skip lines that look like code (have leading whitespace, semicolons, const/let/var, etc.)
|
|
57
|
+
if (/^\s*(const|let|var|this\.|private|public|protected|import|export\s+(default|const|let|function|class)|\/\/|\/\*|\*)/.test(line)) {
|
|
58
|
+
return line; // Code — never redact
|
|
59
|
+
}
|
|
52
60
|
for (const pattern of SENSITIVE_PATTERNS) {
|
|
53
|
-
const match =
|
|
61
|
+
const match = trimmed.match(pattern);
|
|
54
62
|
if (match) {
|
|
55
63
|
return `${match[1]}=[REDACTED]`;
|
|
56
64
|
}
|
package/src/output-processor.ts
CHANGED
|
@@ -123,7 +123,10 @@ RULES:
|
|
|
123
123
|
- Keep errors/failures verbatim
|
|
124
124
|
- Be direct and concise — the user wants an ANSWER, not a data dump
|
|
125
125
|
- 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.
|
|
126
|
-
- For BUILD OUTPUT: if tsc/build exits 0 with no output, it SUCCEEDED. Empty output = success
|
|
126
|
+
- For BUILD OUTPUT: if tsc/build exits 0 with no output, it SUCCEEDED. Empty output = success.
|
|
127
|
+
- For GREP/SEARCH OUTPUT (file:line:match format): List ALL matches grouped by file. NEVER summarize into one sentence. Format: "N matches in M files:" then list each match. The agent needs every match, not a prose interpretation.
|
|
128
|
+
- For FILE LISTINGS (ls, find): show count + key entries. "23 files: src/ai.ts, src/cli.tsx, ..."
|
|
129
|
+
- For GIT LOG/DIFF: preserve commit hashes, file names, and +/- line counts.`;
|
|
127
130
|
|
|
128
131
|
/**
|
|
129
132
|
* Process command output through AI summarization.
|