@hasna/terminal 3.7.1 → 3.7.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.
@@ -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,19 +570,22 @@ 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}` }) }] };
568
579
  }
569
580
  // AI extracts symbols — works for ANY language
570
- const provider = getOutputProvider();
571
- const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
572
- const content = result.content.length > 6000 ? result.content.slice(0, 6000) : result.content;
573
- const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
574
- model: outputModel,
575
- system: `Extract all symbols from this source file. Return ONLY a JSON array, no explanation.
581
+ let symbols = [];
582
+ try {
583
+ const provider = getOutputProvider();
584
+ const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
585
+ const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
586
+ const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
587
+ model: outputModel,
588
+ system: `Extract all symbols from this source file. Return ONLY a JSON array, no explanation.
576
589
 
577
590
  Each symbol: {"name": "symbolName", "kind": "function|class|method|interface|type|variable|export", "line": lineNumber, "signature": "brief signature"}
578
591
 
@@ -580,17 +593,17 @@ For class methods, use "ClassName.methodName" as name with kind "method".
580
593
  Include: functions, classes, methods, interfaces, types, exported constants.
581
594
  Exclude: imports, local variables, comments.
582
595
  Line numbers must be accurate (count from 1).`,
583
- maxTokens: 1000,
584
- temperature: 0,
585
- });
586
- // Parse AI response
587
- let symbols = [];
588
- try {
596
+ maxTokens: 2000,
597
+ temperature: 0,
598
+ });
589
599
  const jsonMatch = summary.match(/\[[\s\S]*\]/);
590
600
  if (jsonMatch)
591
601
  symbols = JSON.parse(jsonMatch[0]);
592
602
  }
593
- catch { }
603
+ catch (err) {
604
+ // Surface the error instead of silently returning []
605
+ return { content: [{ type: "text", text: JSON.stringify({ error: `AI symbol extraction failed: ${err.message?.slice(0, 200)}`, file: filePath }) }] };
606
+ }
594
607
  const outputTokens = estimateTokens(result.content);
595
608
  const symbolTokens = estimateTokens(JSON.stringify(symbols));
596
609
  logCall("symbols", { command: filePath, outputTokens, tokensSaved: Math.max(0, outputTokens - symbolTokens), durationMs: Date.now() - start, aiProcessed: true });
@@ -602,8 +615,9 @@ Line numbers must be accurate (count from 1).`,
602
615
  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
616
  path: z.string().describe("Source file path"),
604
617
  name: z.string().describe("Symbol name (function, class, interface)"),
605
- }, async ({ path: filePath, name }) => {
618
+ }, async ({ path: rawPath, name }) => {
606
619
  const start = Date.now();
620
+ const filePath = resolvePath(rawPath);
607
621
  const result = cachedRead(filePath, {});
608
622
  if (!result.content || result.content.startsWith("Error:")) {
609
623
  return { content: [{ type: "text", text: JSON.stringify({ error: `Cannot read ${filePath}` }) }] };
@@ -711,8 +725,9 @@ Match by function name, class name, method name (including ClassName.method), in
711
725
  find: z.string().describe("Text to find (exact match)"),
712
726
  replace: z.string().describe("Replacement text"),
713
727
  all: z.boolean().optional().describe("Replace all occurrences (default: first only)"),
714
- }, async ({ file, find, replace, all }) => {
728
+ }, async ({ file: rawFile, find, replace, all }) => {
715
729
  const start = Date.now();
730
+ const file = resolvePath(rawFile);
716
731
  const { readFileSync, writeFileSync } = await import("fs");
717
732
  try {
718
733
  let content = readFileSync(file, "utf8");
@@ -738,8 +753,9 @@ Match by function name, class name, method name (including ClassName.method), in
738
753
  file: z.string().describe("File path to search in"),
739
754
  items: z.array(z.string()).describe("Names or patterns to look up"),
740
755
  context: z.number().optional().describe("Lines of context around each match (default: 3)"),
741
- }, async ({ file, items, context }) => {
756
+ }, async ({ file: rawFile, items, context }) => {
742
757
  const start = Date.now();
758
+ const file = resolvePath(rawFile);
743
759
  const { readFileSync } = await import("fs");
744
760
  try {
745
761
  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.3",
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,22 +791,25 @@ 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}` }) }] };
790
800
  }
791
801
 
792
802
  // AI extracts symbols — works for ANY language
793
- const provider = getOutputProvider();
794
- const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
795
- const content = result.content.length > 6000 ? result.content.slice(0, 6000) : result.content;
796
- const summary = await provider.complete(
797
- `File: ${filePath}\n\n${content}`,
798
- {
799
- model: outputModel,
800
- system: `Extract all symbols from this source file. Return ONLY a JSON array, no explanation.
803
+ let symbols: any[] = [];
804
+ try {
805
+ const provider = getOutputProvider();
806
+ const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
807
+ const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
808
+ const summary = await provider.complete(
809
+ `File: ${filePath}\n\n${content}`,
810
+ {
811
+ model: outputModel,
812
+ system: `Extract all symbols from this source file. Return ONLY a JSON array, no explanation.
801
813
 
802
814
  Each symbol: {"name": "symbolName", "kind": "function|class|method|interface|type|variable|export", "line": lineNumber, "signature": "brief signature"}
803
815
 
@@ -805,17 +817,17 @@ For class methods, use "ClassName.methodName" as name with kind "method".
805
817
  Include: functions, classes, methods, interfaces, types, exported constants.
806
818
  Exclude: imports, local variables, comments.
807
819
  Line numbers must be accurate (count from 1).`,
808
- maxTokens: 1000,
809
- temperature: 0,
810
- }
811
- );
820
+ maxTokens: 2000,
821
+ temperature: 0,
822
+ }
823
+ );
812
824
 
813
- // Parse AI response
814
- let symbols: any[] = [];
815
- try {
816
825
  const jsonMatch = summary.match(/\[[\s\S]*\]/);
817
826
  if (jsonMatch) symbols = JSON.parse(jsonMatch[0]);
818
- } catch {}
827
+ } catch (err: any) {
828
+ // Surface the error instead of silently returning []
829
+ return { content: [{ type: "text" as const, text: JSON.stringify({ error: `AI symbol extraction failed: ${err.message?.slice(0, 200)}`, file: filePath }) }] };
830
+ }
819
831
 
820
832
  const outputTokens = estimateTokens(result.content);
821
833
  const symbolTokens = estimateTokens(JSON.stringify(symbols));
@@ -836,8 +848,9 @@ Line numbers must be accurate (count from 1).`,
836
848
  path: z.string().describe("Source file path"),
837
849
  name: z.string().describe("Symbol name (function, class, interface)"),
838
850
  },
839
- async ({ path: filePath, name }) => {
851
+ async ({ path: rawPath, name }) => {
840
852
  const start = Date.now();
853
+ const filePath = resolvePath(rawPath);
841
854
  const result = cachedRead(filePath, {});
842
855
  if (!result.content || result.content.startsWith("Error:")) {
843
856
  return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Cannot read ${filePath}` }) }] };
@@ -966,8 +979,9 @@ Match by function name, class name, method name (including ClassName.method), in
966
979
  replace: z.string().describe("Replacement text"),
967
980
  all: z.boolean().optional().describe("Replace all occurrences (default: first only)"),
968
981
  },
969
- async ({ file, find, replace, all }) => {
982
+ async ({ file: rawFile, find, replace, all }) => {
970
983
  const start = Date.now();
984
+ const file = resolvePath(rawFile);
971
985
  const { readFileSync, writeFileSync } = await import("fs");
972
986
  try {
973
987
  let content = readFileSync(file, "utf8");
@@ -997,8 +1011,9 @@ Match by function name, class name, method name (including ClassName.method), in
997
1011
  items: z.array(z.string()).describe("Names or patterns to look up"),
998
1012
  context: z.number().optional().describe("Lines of context around each match (default: 3)"),
999
1013
  },
1000
- async ({ file, items, context }) => {
1014
+ async ({ file: rawFile, items, context }) => {
1001
1015
  const start = Date.now();
1016
+ const file = resolvePath(rawFile);
1002
1017
  const { readFileSync } = await import("fs");
1003
1018
  try {
1004
1019
  const content = readFileSync(file, "utf8");