@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.
- package/dist/mcp/server.js +34 -18
- package/package.json +1 -1
- package/src/mcp/server.ts +36 -21
package/dist/mcp/server.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
584
|
-
|
|
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:
|
|
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
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:
|
|
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
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
809
|
-
|
|
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:
|
|
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");
|