@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.
- package/dist/mcp/server.js +35 -9
- package/package.json +1 -1
- package/src/mcp/server.ts +37 -9
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,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
|
-
|
|
491
|
-
|
|
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
|
|
516
|
+
summary,
|
|
495
517
|
lines: result.content.split("\n").length,
|
|
496
|
-
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:
|
|
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:
|
|
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
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
|
-
|
|
694
|
-
|
|
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
|
|
721
|
+
summary,
|
|
698
722
|
lines: result.content.split("\n").length,
|
|
699
|
-
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:
|
|
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:
|
|
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");
|