@hasna/terminal 3.6.0 → 3.7.1

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.
@@ -153,7 +153,7 @@ export function createServer() {
153
153
  return { content: [{ type: "text", text: output }] };
154
154
  });
155
155
  // ── execute_smart: AI-powered output processing ────────────────────────────
156
- server.tool("execute_smart", "Run a command and get AI-summarized output. The AI decides what's important errors, failures, key results are kept; verbose logs, progress bars, passing tests are dropped. Saves 80-95% tokens vs raw output. Best tool for agents.", {
156
+ server.tool("execute_smart", "Run a command and get AI-summarized output (80-95% token savings). Use this for: test runs, builds, git operations, process management, system info. Do NOT use for file read/write use your native Read/Write/Edit tools instead (they're faster, no shell overhead).", {
157
157
  command: z.string().describe("Shell command to execute"),
158
158
  cwd: z.string().optional().describe("Working directory"),
159
159
  timeout: z.number().optional().describe("Timeout in ms (default: 30000)"),
@@ -478,7 +478,7 @@ export function createServer() {
478
478
  };
479
479
  });
480
480
  // ── read_file: cached file reading ─────────────────────────────────────────
481
- 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.", {
481
+ server.tool("read_file", "Read a file with summarize=true for AI outline (~90% fewer tokens). For full file reads without summarization, prefer your native Read tool (faster, no MCP overhead). Use this when you want cached reads or AI summaries.", {
482
482
  path: z.string().describe("File path"),
483
483
  offset: z.number().optional().describe("Start line (0-indexed)"),
484
484
  limit: z.number().optional().describe("Max lines to return"),
@@ -487,13 +487,25 @@ export function createServer() {
487
487
  const start = Date.now();
488
488
  const result = cachedRead(path, { offset, limit });
489
489
  if (summarize && result.content.length > 500) {
490
- const processed = await processOutput(`cat ${path}`, result.content);
491
- logCall("read_file", { command: path, outputTokens: estimateTokens(result.content), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, aiProcessed: true });
490
+ // AI-native file summary ask directly what the file does
491
+ const provider = getOutputProvider();
492
+ const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
493
+ const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
494
+ const summary = await provider.complete(`File: ${path}\n\n${content}`, {
495
+ model: outputModel,
496
+ system: `Describe what this source file does in 2-4 lines. Include: main class/module name, key methods/functions, what it exports, and its purpose. Be specific — name the actual functions and what they do. Never just say "N lines of code."`,
497
+ maxTokens: 300,
498
+ temperature: 0.2,
499
+ });
500
+ const outputTokens = estimateTokens(result.content);
501
+ const summaryTokens = estimateTokens(summary);
502
+ const saved = Math.max(0, outputTokens - summaryTokens);
503
+ logCall("read_file", { command: path, outputTokens, tokensSaved: saved, durationMs: Date.now() - start, aiProcessed: true });
492
504
  return {
493
505
  content: [{ type: "text", text: JSON.stringify({
494
- summary: processed.summary,
506
+ summary,
495
507
  lines: result.content.split("\n").length,
496
- tokensSaved: processed.tokensSaved,
508
+ tokensSaved: saved,
497
509
  cached: result.cached,
498
510
  }) }],
499
511
  };
@@ -694,7 +706,7 @@ Match by function name, class name, method name (including ClassName.method), in
694
706
  tokensSaved: processed.tokensSaved,
695
707
  }) }] };
696
708
  });
697
- server.tool("edit", "Find and replace text in a file. Agent says what to change, no sed/awk/python needed. Saves ~200 tokens vs constructing shell commands.", {
709
+ server.tool("edit", "Find and replace in a file. For simple edits, prefer your native Edit tool (faster). Use this for batch replacements (all=true) or when you don't have a native Edit tool available.", {
698
710
  file: z.string().describe("File path"),
699
711
  find: z.string().describe("Text to find (exact match)"),
700
712
  replace: z.string().describe("Replacement text"),
@@ -24,14 +24,31 @@ function saveStore(filePath, store) {
24
24
  mkdirSync(dir, { recursive: true });
25
25
  writeFileSync(filePath, JSON.stringify(store, null, 2));
26
26
  }
27
+ // ── Built-in system recipes (always available, zero config) ─────────────────
28
+ const SYSTEM_RECIPES = [
29
+ // Git workflows
30
+ { id: "sys-commit-push", name: "commit-push", description: "Stage all, commit, push to origin", command: "git add -A && git commit -m \"{message}\" && git push", tags: ["git"], collection: "git", variables: [{ name: "message", required: true }], createdAt: 0, updatedAt: 0 },
31
+ { id: "sys-pr", name: "create-pr", description: "Create GitHub PR from current branch", command: "gh pr create --title \"{title}\" --body \"{body}\"", tags: ["git", "github"], collection: "git", variables: [{ name: "title", required: true }, { name: "body", default: "" }], createdAt: 0, updatedAt: 0 },
32
+ { id: "sys-stash", name: "stash-switch", description: "Stash changes, switch branch, pop", command: "git stash && git checkout {branch} && git stash pop", tags: ["git"], collection: "git", variables: [{ name: "branch", required: true }], createdAt: 0, updatedAt: 0 },
33
+ // Quality checks
34
+ { id: "sys-todos", name: "find-todos", description: "Find all TODO/FIXME/HACK in source code", command: "grep -rn 'TODO\\|FIXME\\|HACK\\|XXX' {path} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.py' --include='*.go' --include='*.rs' --include='*.java' --include='*.rb'", tags: ["quality"], collection: "quality", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
35
+ { id: "sys-deadcode", name: "find-unused-exports", description: "Find exported symbols that may be unused", command: "grep -rn 'export ' {path} --include='*.ts' | sed 's/.*export //' | sed 's/[(<:].*//' | sort -u", tags: ["quality"], collection: "quality", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
36
+ { id: "sys-security", name: "security-scan", description: "Scan for common security anti-patterns", command: "grep -rn 'eval\\|exec\\|spawn\\|innerHTML\\|dangerouslySetInnerHTML\\|password.*=.*[\"'\\']' {path} --include='*.ts' --include='*.js' --include='*.py'", tags: ["security"], collection: "quality", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
37
+ // Project info
38
+ { id: "sys-deps", name: "list-deps", description: "Show project dependencies", command: "cat package.json 2>/dev/null | grep -A 100 '\"dependencies\"' | head -30 || cat requirements.txt 2>/dev/null || cat Cargo.toml 2>/dev/null | grep -A 50 'dependencies'", tags: ["deps"], collection: "project", variables: [], createdAt: 0, updatedAt: 0 },
39
+ { id: "sys-size", name: "project-size", description: "Count lines of code by file type", command: "find {path} -not -path '*/node_modules/*' -not -path '*/dist/*' -not -path '*/.git/*' -type f | xargs wc -l 2>/dev/null | sort -rn | head -20", tags: ["stats"], collection: "project", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
40
+ // Process management
41
+ { id: "sys-port", name: "kill-port", description: "Kill whatever is running on a port", command: "lsof -ti :{port} | xargs kill -9 2>/dev/null || echo 'Port {port} is free'", tags: ["process"], collection: "system", variables: [{ name: "port", required: true }], createdAt: 0, updatedAt: 0 },
42
+ { id: "sys-disk", name: "disk-usage", description: "Show disk usage of current directory", command: "du -sh {path}/* 2>/dev/null | sort -rh | head -15", tags: ["system"], collection: "system", variables: [{ name: "path", default: "." }], createdAt: 0, updatedAt: 0 },
43
+ ];
27
44
  // ── CRUD operations ──────────────────────────────────────────────────────────
28
- /** Get all recipes (merged: global + project-scoped) */
45
+ /** Get all recipes (merged: system + global + project-scoped) */
29
46
  export function listRecipes(projectPath) {
30
47
  const global = loadStore(GLOBAL_FILE).recipes;
31
48
  if (!projectPath)
32
- return global;
49
+ return [...global, ...SYSTEM_RECIPES];
33
50
  const project = loadStore(projectFile(projectPath)).recipes;
34
- return [...project, ...global]; // project recipes first (higher priority)
51
+ return [...project, ...global, ...SYSTEM_RECIPES]; // project > global > system priority
35
52
  }
36
53
  /** Get recipes filtered by collection */
37
54
  export function listByCollection(collection, projectPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "3.6.0",
3
+ "version": "3.7.1",
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
@@ -181,7 +181,7 @@ export function createServer(): McpServer {
181
181
 
182
182
  server.tool(
183
183
  "execute_smart",
184
- "Run a command and get AI-summarized output. The AI decides what's important errors, failures, key results are kept; verbose logs, progress bars, passing tests are dropped. Saves 80-95% tokens vs raw output. Best tool for agents.",
184
+ "Run a command and get AI-summarized output (80-95% token savings). Use this for: test runs, builds, git operations, process management, system info. Do NOT use for file read/write use your native Read/Write/Edit tools instead (they're faster, no shell overhead).",
185
185
  {
186
186
  command: z.string().describe("Shell command to execute"),
187
187
  cwd: z.string().optional().describe("Working directory"),
@@ -678,7 +678,7 @@ export function createServer(): McpServer {
678
678
 
679
679
  server.tool(
680
680
  "read_file",
681
- "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.",
681
+ "Read a file with summarize=true for AI outline (~90% fewer tokens). For full file reads without summarization, prefer your native Read tool (faster, no MCP overhead). Use this when you want cached reads or AI summaries.",
682
682
  {
683
683
  path: z.string().describe("File path"),
684
684
  offset: z.number().optional().describe("Start line (0-indexed)"),
@@ -690,13 +690,28 @@ export function createServer(): McpServer {
690
690
  const result = cachedRead(path, { offset, limit });
691
691
 
692
692
  if (summarize && result.content.length > 500) {
693
- const processed = await processOutput(`cat ${path}`, result.content);
694
- logCall("read_file", { command: path, outputTokens: estimateTokens(result.content), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, aiProcessed: true });
693
+ // AI-native file summary ask directly what the file does
694
+ const provider = getOutputProvider();
695
+ const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
696
+ const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
697
+ const summary = await provider.complete(
698
+ `File: ${path}\n\n${content}`,
699
+ {
700
+ model: outputModel,
701
+ system: `Describe what this source file does in 2-4 lines. Include: main class/module name, key methods/functions, what it exports, and its purpose. Be specific — name the actual functions and what they do. Never just say "N lines of code."`,
702
+ maxTokens: 300,
703
+ temperature: 0.2,
704
+ }
705
+ );
706
+ const outputTokens = estimateTokens(result.content);
707
+ const summaryTokens = estimateTokens(summary);
708
+ const saved = Math.max(0, outputTokens - summaryTokens);
709
+ logCall("read_file", { command: path, outputTokens, tokensSaved: saved, durationMs: Date.now() - start, aiProcessed: true });
695
710
  return {
696
711
  content: [{ type: "text" as const, text: JSON.stringify({
697
- summary: processed.summary,
712
+ summary,
698
713
  lines: result.content.split("\n").length,
699
- tokensSaved: processed.tokensSaved,
714
+ tokensSaved: saved,
700
715
  cached: result.cached,
701
716
  }) }],
702
717
  };
@@ -944,7 +959,7 @@ Match by function name, class name, method name (including ClassName.method), in
944
959
 
945
960
  server.tool(
946
961
  "edit",
947
- "Find and replace text in a file. Agent says what to change, no sed/awk/python needed. Saves ~200 tokens vs constructing shell commands.",
962
+ "Find and replace in a file. For simple edits, prefer your native Edit tool (faster). Use this for batch replacements (all=true) or when you don't have a native Edit tool available.",
948
963
  {
949
964
  file: z.string().describe("File path"),
950
965
  find: z.string().describe("Text to find (exact match)"),
@@ -28,14 +28,36 @@ function saveStore(filePath: string, store: RecipeStore): void {
28
28
  writeFileSync(filePath, JSON.stringify(store, null, 2));
29
29
  }
30
30
 
31
+ // ── Built-in system recipes (always available, zero config) ─────────────────
32
+
33
+ const SYSTEM_RECIPES: Recipe[] = [
34
+ // Git workflows
35
+ { id: "sys-commit-push", name: "commit-push", description: "Stage all, commit, push to origin", command: "git add -A && git commit -m \"{message}\" && git push", tags: ["git"], collection: "git", variables: [{ name: "message", required: true }], createdAt: 0, updatedAt: 0 },
36
+ { id: "sys-pr", name: "create-pr", description: "Create GitHub PR from current branch", command: "gh pr create --title \"{title}\" --body \"{body}\"", tags: ["git", "github"], collection: "git", variables: [{ name: "title", required: true }, { name: "body", default: "" }], createdAt: 0, updatedAt: 0 },
37
+ { id: "sys-stash", name: "stash-switch", description: "Stash changes, switch branch, pop", command: "git stash && git checkout {branch} && git stash pop", tags: ["git"], collection: "git", variables: [{ name: "branch", required: true }], createdAt: 0, updatedAt: 0 },
38
+
39
+ // Quality checks
40
+ { id: "sys-todos", name: "find-todos", description: "Find all TODO/FIXME/HACK in source code", command: "grep -rn 'TODO\\|FIXME\\|HACK\\|XXX' {path} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.py' --include='*.go' --include='*.rs' --include='*.java' --include='*.rb'", tags: ["quality"], collection: "quality", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
41
+ { id: "sys-deadcode", name: "find-unused-exports", description: "Find exported symbols that may be unused", command: "grep -rn 'export ' {path} --include='*.ts' | sed 's/.*export //' | sed 's/[(<:].*//' | sort -u", tags: ["quality"], collection: "quality", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
42
+ { id: "sys-security", name: "security-scan", description: "Scan for common security anti-patterns", command: "grep -rn 'eval\\|exec\\|spawn\\|innerHTML\\|dangerouslySetInnerHTML\\|password.*=.*[\"'\\']' {path} --include='*.ts' --include='*.js' --include='*.py'", tags: ["security"], collection: "quality", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
43
+
44
+ // Project info
45
+ { id: "sys-deps", name: "list-deps", description: "Show project dependencies", command: "cat package.json 2>/dev/null | grep -A 100 '\"dependencies\"' | head -30 || cat requirements.txt 2>/dev/null || cat Cargo.toml 2>/dev/null | grep -A 50 'dependencies'", tags: ["deps"], collection: "project", variables: [], createdAt: 0, updatedAt: 0 },
46
+ { id: "sys-size", name: "project-size", description: "Count lines of code by file type", command: "find {path} -not -path '*/node_modules/*' -not -path '*/dist/*' -not -path '*/.git/*' -type f | xargs wc -l 2>/dev/null | sort -rn | head -20", tags: ["stats"], collection: "project", variables: [{ name: "path", default: "src/" }], createdAt: 0, updatedAt: 0 },
47
+
48
+ // Process management
49
+ { id: "sys-port", name: "kill-port", description: "Kill whatever is running on a port", command: "lsof -ti :{port} | xargs kill -9 2>/dev/null || echo 'Port {port} is free'", tags: ["process"], collection: "system", variables: [{ name: "port", required: true }], createdAt: 0, updatedAt: 0 },
50
+ { id: "sys-disk", name: "disk-usage", description: "Show disk usage of current directory", command: "du -sh {path}/* 2>/dev/null | sort -rh | head -15", tags: ["system"], collection: "system", variables: [{ name: "path", default: "." }], createdAt: 0, updatedAt: 0 },
51
+ ];
52
+
31
53
  // ── CRUD operations ──────────────────────────────────────────────────────────
32
54
 
33
- /** Get all recipes (merged: global + project-scoped) */
55
+ /** Get all recipes (merged: system + global + project-scoped) */
34
56
  export function listRecipes(projectPath?: string): Recipe[] {
35
57
  const global = loadStore(GLOBAL_FILE).recipes;
36
- if (!projectPath) return global;
58
+ if (!projectPath) return [...global, ...SYSTEM_RECIPES];
37
59
  const project = loadStore(projectFile(projectPath)).recipes;
38
- return [...project, ...global]; // project recipes first (higher priority)
60
+ return [...project, ...global, ...SYSTEM_RECIPES]; // project > global > system priority
39
61
  }
40
62
 
41
63
  /** Get recipes filtered by collection */