@hasna/terminal 3.7.1 → 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,8 +492,9 @@ 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
500
  // AI-native file summary — ask directly what the file does
@@ -560,8 +570,9 @@ export function createServer() {
560
570
  // ── symbols: file structure outline ───────────────────────────────────────
561
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.", {
562
572
  path: z.string().describe("File path to extract symbols from"),
563
- }, async ({ path: filePath }) => {
573
+ }, async ({ path: rawPath }) => {
564
574
  const start = Date.now();
575
+ const filePath = resolvePath(rawPath);
565
576
  const result = cachedRead(filePath, {});
566
577
  if (!result.content || result.content.startsWith("Error:")) {
567
578
  return { content: [{ type: "text", text: JSON.stringify({ error: `Cannot read ${filePath}` }) }] };
@@ -602,8 +613,9 @@ Line numbers must be accurate (count from 1).`,
602
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.", {
603
614
  path: z.string().describe("Source file path"),
604
615
  name: z.string().describe("Symbol name (function, class, interface)"),
605
- }, async ({ path: filePath, name }) => {
616
+ }, async ({ path: rawPath, name }) => {
606
617
  const start = Date.now();
618
+ const filePath = resolvePath(rawPath);
607
619
  const result = cachedRead(filePath, {});
608
620
  if (!result.content || result.content.startsWith("Error:")) {
609
621
  return { content: [{ type: "text", text: JSON.stringify({ error: `Cannot read ${filePath}` }) }] };
@@ -711,8 +723,9 @@ Match by function name, class name, method name (including ClassName.method), in
711
723
  find: z.string().describe("Text to find (exact match)"),
712
724
  replace: z.string().describe("Replacement text"),
713
725
  all: z.boolean().optional().describe("Replace all occurrences (default: first only)"),
714
- }, async ({ file, find, replace, all }) => {
726
+ }, async ({ file: rawFile, find, replace, all }) => {
715
727
  const start = Date.now();
728
+ const file = resolvePath(rawFile);
716
729
  const { readFileSync, writeFileSync } = await import("fs");
717
730
  try {
718
731
  let content = readFileSync(file, "utf8");
@@ -738,8 +751,9 @@ Match by function name, class name, method name (including ClassName.method), in
738
751
  file: z.string().describe("File path to search in"),
739
752
  items: z.array(z.string()).describe("Names or patterns to look up"),
740
753
  context: z.number().optional().describe("Lines of context around each match (default: 3)"),
741
- }, async ({ file, items, context }) => {
754
+ }, async ({ file: rawFile, items, context }) => {
742
755
  const start = Date.now();
756
+ const file = resolvePath(rawFile);
743
757
  const { readFileSync } = await import("fs");
744
758
  try {
745
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.1",
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,8 +693,9 @@ 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) {
@@ -782,8 +791,9 @@ export function createServer(): McpServer {
782
791
  {
783
792
  path: z.string().describe("File path to extract symbols from"),
784
793
  },
785
- async ({ path: filePath }) => {
794
+ async ({ path: rawPath }) => {
786
795
  const start = Date.now();
796
+ const filePath = resolvePath(rawPath);
787
797
  const result = cachedRead(filePath, {});
788
798
  if (!result.content || result.content.startsWith("Error:")) {
789
799
  return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Cannot read ${filePath}` }) }] };
@@ -836,8 +846,9 @@ Line numbers must be accurate (count from 1).`,
836
846
  path: z.string().describe("Source file path"),
837
847
  name: z.string().describe("Symbol name (function, class, interface)"),
838
848
  },
839
- async ({ path: filePath, name }) => {
849
+ async ({ path: rawPath, name }) => {
840
850
  const start = Date.now();
851
+ const filePath = resolvePath(rawPath);
841
852
  const result = cachedRead(filePath, {});
842
853
  if (!result.content || result.content.startsWith("Error:")) {
843
854
  return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Cannot read ${filePath}` }) }] };
@@ -966,8 +977,9 @@ Match by function name, class name, method name (including ClassName.method), in
966
977
  replace: z.string().describe("Replacement text"),
967
978
  all: z.boolean().optional().describe("Replace all occurrences (default: first only)"),
968
979
  },
969
- async ({ file, find, replace, all }) => {
980
+ async ({ file: rawFile, find, replace, all }) => {
970
981
  const start = Date.now();
982
+ const file = resolvePath(rawFile);
971
983
  const { readFileSync, writeFileSync } = await import("fs");
972
984
  try {
973
985
  let content = readFileSync(file, "utf8");
@@ -997,8 +1009,9 @@ Match by function name, class name, method name (including ClassName.method), in
997
1009
  items: z.array(z.string()).describe("Names or patterns to look up"),
998
1010
  context: z.number().optional().describe("Lines of context around each match (default: 3)"),
999
1011
  },
1000
- async ({ file, items, context }) => {
1012
+ async ({ file: rawFile, items, context }) => {
1001
1013
  const start = Date.now();
1014
+ const file = resolvePath(rawFile);
1002
1015
  const { readFileSync } = await import("fs");
1003
1016
  try {
1004
1017
  const content = readFileSync(file, "utf8");