@hasna/terminal 3.7.0 → 3.7.2

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.
@@ -54,6 +54,15 @@ function exec(command, cwd, timeout, allowRewrite = false) {
54
54
  });
55
55
  });
56
56
  }
57
+ /** Resolve a path — supports relative paths against cwd, just like a shell */
58
+ function resolvePath(p, cwd) {
59
+ if (!p)
60
+ return cwd ?? process.cwd();
61
+ if (p.startsWith("/") || p.startsWith("~"))
62
+ return p;
63
+ const { join } = require("path");
64
+ return join(cwd ?? process.cwd(), p);
65
+ }
57
66
  // ── server ───────────────────────────────────────────────────────────────────
58
67
  export function createServer() {
59
68
  const server = new McpServer({
@@ -483,17 +492,30 @@ export function createServer() {
483
492
  offset: z.number().optional().describe("Start line (0-indexed)"),
484
493
  limit: z.number().optional().describe("Max lines to return"),
485
494
  summarize: z.boolean().optional().describe("Return AI summary instead of full content (saves ~90% tokens)"),
486
- }, async ({ path, offset, limit, summarize }) => {
495
+ }, async ({ path: rawPath, offset, limit, summarize }) => {
487
496
  const start = Date.now();
497
+ const path = resolvePath(rawPath);
488
498
  const result = cachedRead(path, { offset, limit });
489
499
  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 });
500
+ // AI-native file summary ask directly what the file does
501
+ const provider = getOutputProvider();
502
+ const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
503
+ const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
504
+ const summary = await provider.complete(`File: ${path}\n\n${content}`, {
505
+ model: outputModel,
506
+ 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."`,
507
+ maxTokens: 300,
508
+ temperature: 0.2,
509
+ });
510
+ const outputTokens = estimateTokens(result.content);
511
+ const summaryTokens = estimateTokens(summary);
512
+ const saved = Math.max(0, outputTokens - summaryTokens);
513
+ logCall("read_file", { command: path, outputTokens, tokensSaved: saved, durationMs: Date.now() - start, aiProcessed: true });
492
514
  return {
493
515
  content: [{ type: "text", text: JSON.stringify({
494
- summary: processed.summary,
516
+ summary,
495
517
  lines: result.content.split("\n").length,
496
- tokensSaved: processed.tokensSaved,
518
+ tokensSaved: saved,
497
519
  cached: result.cached,
498
520
  }) }],
499
521
  };
@@ -548,8 +570,9 @@ export function createServer() {
548
570
  // ── symbols: file structure outline ───────────────────────────────────────
549
571
  server.tool("symbols", "Get a structured outline of any source file — functions, classes, methods, interfaces, exports with line numbers. Works for ALL languages (TypeScript, Python, Go, Rust, Java, C#, Ruby, PHP, etc.). AI-powered, not regex.", {
550
572
  path: z.string().describe("File path to extract symbols from"),
551
- }, async ({ path: filePath }) => {
573
+ }, async ({ path: rawPath }) => {
552
574
  const start = Date.now();
575
+ const filePath = resolvePath(rawPath);
553
576
  const result = cachedRead(filePath, {});
554
577
  if (!result.content || result.content.startsWith("Error:")) {
555
578
  return { content: [{ type: "text", text: JSON.stringify({ error: `Cannot read ${filePath}` }) }] };
@@ -590,8 +613,9 @@ Line numbers must be accurate (count from 1).`,
590
613
  server.tool("read_symbol", "Read a specific function, class, or interface by name from a source file. Returns only the code block — not the entire file. Saves 70-85% tokens vs reading the whole file.", {
591
614
  path: z.string().describe("Source file path"),
592
615
  name: z.string().describe("Symbol name (function, class, interface)"),
593
- }, async ({ path: filePath, name }) => {
616
+ }, async ({ path: rawPath, name }) => {
594
617
  const start = Date.now();
618
+ const filePath = resolvePath(rawPath);
595
619
  const result = cachedRead(filePath, {});
596
620
  if (!result.content || result.content.startsWith("Error:")) {
597
621
  return { content: [{ type: "text", text: JSON.stringify({ error: `Cannot read ${filePath}` }) }] };
@@ -699,8 +723,9 @@ Match by function name, class name, method name (including ClassName.method), in
699
723
  find: z.string().describe("Text to find (exact match)"),
700
724
  replace: z.string().describe("Replacement text"),
701
725
  all: z.boolean().optional().describe("Replace all occurrences (default: first only)"),
702
- }, async ({ file, find, replace, all }) => {
726
+ }, async ({ file: rawFile, find, replace, all }) => {
703
727
  const start = Date.now();
728
+ const file = resolvePath(rawFile);
704
729
  const { readFileSync, writeFileSync } = await import("fs");
705
730
  try {
706
731
  let content = readFileSync(file, "utf8");
@@ -726,8 +751,9 @@ Match by function name, class name, method name (including ClassName.method), in
726
751
  file: z.string().describe("File path to search in"),
727
752
  items: z.array(z.string()).describe("Names or patterns to look up"),
728
753
  context: z.number().optional().describe("Lines of context around each match (default: 3)"),
729
- }, async ({ file, items, context }) => {
754
+ }, async ({ file: rawFile, items, context }) => {
730
755
  const start = Date.now();
756
+ const file = resolvePath(rawFile);
731
757
  const { readFileSync } = await import("fs");
732
758
  try {
733
759
  const content = readFileSync(file, "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "3.7.0",
3
+ "version": "3.7.2",
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
@@ -58,6 +58,14 @@ function exec(command: string, cwd?: string, timeout?: number, allowRewrite: boo
58
58
  });
59
59
  }
60
60
 
61
+ /** Resolve a path — supports relative paths against cwd, just like a shell */
62
+ function resolvePath(p: string, cwd?: string): string {
63
+ if (!p) return cwd ?? process.cwd();
64
+ if (p.startsWith("/") || p.startsWith("~")) return p;
65
+ const { join } = require("path");
66
+ return join(cwd ?? process.cwd(), p);
67
+ }
68
+
61
69
  // ── server ───────────────────────────────────────────────────────────────────
62
70
 
63
71
  export function createServer(): McpServer {
@@ -685,18 +693,34 @@ export function createServer(): McpServer {
685
693
  limit: z.number().optional().describe("Max lines to return"),
686
694
  summarize: z.boolean().optional().describe("Return AI summary instead of full content (saves ~90% tokens)"),
687
695
  },
688
- async ({ path, offset, limit, summarize }) => {
696
+ async ({ path: rawPath, offset, limit, summarize }) => {
689
697
  const start = Date.now();
698
+ const path = resolvePath(rawPath);
690
699
  const result = cachedRead(path, { offset, limit });
691
700
 
692
701
  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 });
702
+ // AI-native file summary ask directly what the file does
703
+ const provider = getOutputProvider();
704
+ const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
705
+ const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
706
+ const summary = await provider.complete(
707
+ `File: ${path}\n\n${content}`,
708
+ {
709
+ model: outputModel,
710
+ 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."`,
711
+ maxTokens: 300,
712
+ temperature: 0.2,
713
+ }
714
+ );
715
+ const outputTokens = estimateTokens(result.content);
716
+ const summaryTokens = estimateTokens(summary);
717
+ const saved = Math.max(0, outputTokens - summaryTokens);
718
+ logCall("read_file", { command: path, outputTokens, tokensSaved: saved, durationMs: Date.now() - start, aiProcessed: true });
695
719
  return {
696
720
  content: [{ type: "text" as const, text: JSON.stringify({
697
- summary: processed.summary,
721
+ summary,
698
722
  lines: result.content.split("\n").length,
699
- tokensSaved: processed.tokensSaved,
723
+ tokensSaved: saved,
700
724
  cached: result.cached,
701
725
  }) }],
702
726
  };
@@ -767,8 +791,9 @@ export function createServer(): McpServer {
767
791
  {
768
792
  path: z.string().describe("File path to extract symbols from"),
769
793
  },
770
- async ({ path: filePath }) => {
794
+ async ({ path: rawPath }) => {
771
795
  const start = Date.now();
796
+ const filePath = resolvePath(rawPath);
772
797
  const result = cachedRead(filePath, {});
773
798
  if (!result.content || result.content.startsWith("Error:")) {
774
799
  return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Cannot read ${filePath}` }) }] };
@@ -821,8 +846,9 @@ Line numbers must be accurate (count from 1).`,
821
846
  path: z.string().describe("Source file path"),
822
847
  name: z.string().describe("Symbol name (function, class, interface)"),
823
848
  },
824
- async ({ path: filePath, name }) => {
849
+ async ({ path: rawPath, name }) => {
825
850
  const start = Date.now();
851
+ const filePath = resolvePath(rawPath);
826
852
  const result = cachedRead(filePath, {});
827
853
  if (!result.content || result.content.startsWith("Error:")) {
828
854
  return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Cannot read ${filePath}` }) }] };
@@ -951,8 +977,9 @@ Match by function name, class name, method name (including ClassName.method), in
951
977
  replace: z.string().describe("Replacement text"),
952
978
  all: z.boolean().optional().describe("Replace all occurrences (default: first only)"),
953
979
  },
954
- async ({ file, find, replace, all }) => {
980
+ async ({ file: rawFile, find, replace, all }) => {
955
981
  const start = Date.now();
982
+ const file = resolvePath(rawFile);
956
983
  const { readFileSync, writeFileSync } = await import("fs");
957
984
  try {
958
985
  let content = readFileSync(file, "utf8");
@@ -982,8 +1009,9 @@ Match by function name, class name, method name (including ClassName.method), in
982
1009
  items: z.array(z.string()).describe("Names or patterns to look up"),
983
1010
  context: z.number().optional().describe("Lines of context around each match (default: 3)"),
984
1011
  },
985
- async ({ file, items, context }) => {
1012
+ async ({ file: rawFile, items, context }) => {
986
1013
  const start = Date.now();
1014
+ const file = resolvePath(rawFile);
987
1015
  const { readFileSync } = await import("fs");
988
1016
  try {
989
1017
  const content = readFileSync(file, "utf8");