@hasna/terminal 3.3.2 → 3.3.4

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.
@@ -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 the result. Supports structured output parsing (json), token compression (compressed), and AI summarization (summary).", {
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)"),
@@ -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. Second read of unchanged file returns instantly from cache. Supports offset/limit for pagination without re-reading.", {
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
- }, async ({ path, offset, limit }) => {
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,
@@ -15,8 +15,12 @@ const MAX_OUTPUT_FOR_AI = 6000;
15
15
  function fingerprint(command, output, exitCode) {
16
16
  const trimmed = output.trim();
17
17
  const lines = trimmed.split("\n").filter(l => l.trim());
18
- // Empty output with success = command succeeded silently (build, lint, etc.)
18
+ // Empty output with success provide context-aware confirmation
19
19
  if (lines.length === 0 && (exitCode === 0 || exitCode === undefined)) {
20
+ // Write commands get a specific confirmation
21
+ if (/\btee\b|>\s*\S|>>|cat\s*<<|echo\s.*>|sed\s+-i|cp\b|mv\b|mkdir\b|touch\b/.test(command)) {
22
+ return "✓ Write succeeded (no output)";
23
+ }
20
24
  return "✓ Success (no output)";
21
25
  }
22
26
  // Single-line trivial outputs — pass through without AI
@@ -90,7 +94,10 @@ RULES:
90
94
  - Keep errors/failures verbatim
91
95
  - Be direct and concise — the user wants an ANSWER, not a data dump
92
96
  - 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.`;
97
+ - For BUILD OUTPUT: if tsc/build exits 0 with no output, it SUCCEEDED. Empty output = success.
98
+ - 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.
99
+ - For FILE LISTINGS (ls, find): show count + key entries. "23 files: src/ai.ts, src/cli.tsx, ..."
100
+ - For GIT LOG/DIFF: preserve commit hashes, file names, and +/- line counts.`;
94
101
  /**
95
102
  * Process command output through AI summarization.
96
103
  * Cheap AI call (~100 tokens) saves 1000+ tokens downstream.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "3.3.2",
3
+ "version": "3.3.4",
4
4
  "description": "Smart terminal wrapper for AI agents and humans — structured output, token compression, MCP server, natural language",
5
5
  "type": "module",
6
6
  "files": [
package/src/mcp/server.ts CHANGED
@@ -69,7 +69,7 @@ export function createServer(): McpServer {
69
69
 
70
70
  server.tool(
71
71
  "execute",
72
- "Run a shell command and return the result. Supports structured output parsing (json), token compression (compressed), and AI summarization (summary).",
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)"),
@@ -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. Second read of unchanged file returns instantly from cache. Supports offset/limit for pagination without re-reading.",
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,
@@ -41,8 +41,12 @@ function fingerprint(command: string, output: string, exitCode?: number): string
41
41
  const trimmed = output.trim();
42
42
  const lines = trimmed.split("\n").filter(l => l.trim());
43
43
 
44
- // Empty output with success = command succeeded silently (build, lint, etc.)
44
+ // Empty output with success provide context-aware confirmation
45
45
  if (lines.length === 0 && (exitCode === 0 || exitCode === undefined)) {
46
+ // Write commands get a specific confirmation
47
+ if (/\btee\b|>\s*\S|>>|cat\s*<<|echo\s.*>|sed\s+-i|cp\b|mv\b|mkdir\b|touch\b/.test(command)) {
48
+ return "✓ Write succeeded (no output)";
49
+ }
46
50
  return "✓ Success (no output)";
47
51
  }
48
52
 
@@ -123,7 +127,10 @@ RULES:
123
127
  - Keep errors/failures verbatim
124
128
  - Be direct and concise — the user wants an ANSWER, not a data dump
125
129
  - 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.`;
130
+ - For BUILD OUTPUT: if tsc/build exits 0 with no output, it SUCCEEDED. Empty output = success.
131
+ - 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.
132
+ - For FILE LISTINGS (ls, find): show count + key entries. "23 files: src/ai.ts, src/cli.tsx, ..."
133
+ - For GIT LOG/DIFF: preserve commit hashes, file names, and +/- line counts.`;
127
134
 
128
135
  /**
129
136
  * Process command output through AI summarization.