@hasna/terminal 4.1.0 → 4.2.0
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 +90 -3
- package/package.json +1 -1
- package/src/mcp/server.ts +87 -3
package/dist/mcp/server.js
CHANGED
|
@@ -498,18 +498,21 @@ export function createServer() {
|
|
|
498
498
|
offset: z.number().optional().describe("Start line (0-indexed)"),
|
|
499
499
|
limit: z.number().optional().describe("Max lines to return"),
|
|
500
500
|
summarize: z.boolean().optional().describe("Return AI summary instead of full content (saves ~90% tokens)"),
|
|
501
|
-
|
|
501
|
+
focus: z.string().optional().describe("Focus hint for summary (e.g., 'public API', 'error handling', 'auth logic')"),
|
|
502
|
+
}, async ({ path: rawPath, offset, limit, summarize, focus }) => {
|
|
502
503
|
const start = Date.now();
|
|
503
504
|
const path = resolvePath(rawPath);
|
|
504
505
|
const result = cachedRead(path, { offset, limit });
|
|
505
506
|
if (summarize && result.content.length > 500) {
|
|
506
|
-
// AI-native file summary — ask directly what the file does
|
|
507
507
|
const provider = getOutputProvider();
|
|
508
508
|
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
509
509
|
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
510
|
+
const focusInstruction = focus
|
|
511
|
+
? `Focus specifically on: ${focus}. Describe only aspects related to "${focus}".`
|
|
512
|
+
: `Describe what this source file does in 2-4 lines. Include: main class/module name, key methods/functions, what it exports, and its purpose.`;
|
|
510
513
|
const summary = await provider.complete(`File: ${path}\n\n${content}`, {
|
|
511
514
|
model: outputModel,
|
|
512
|
-
system:
|
|
515
|
+
system: `${focusInstruction} Be specific — name the actual functions and what they do. Never just say "N lines of code."`,
|
|
513
516
|
maxTokens: 300,
|
|
514
517
|
temperature: 0.2,
|
|
515
518
|
});
|
|
@@ -1208,6 +1211,90 @@ Be specific, not generic. Only flag real problems.`,
|
|
|
1208
1211
|
});
|
|
1209
1212
|
return { content: [{ type: "text", text: recommendation }] };
|
|
1210
1213
|
});
|
|
1214
|
+
// ── batch: multiple operations in one round trip ───────────────────────────
|
|
1215
|
+
server.tool("batch", "Run multiple operations in ONE call. Saves N-1 round trips. Each op can be: execute (run command), read (file read/summarize), search (grep pattern), or symbols (file outline).", {
|
|
1216
|
+
ops: z.array(z.object({
|
|
1217
|
+
type: z.enum(["execute", "read", "search", "symbols"]).describe("Operation type"),
|
|
1218
|
+
command: z.string().optional().describe("Shell command (for execute)"),
|
|
1219
|
+
path: z.string().optional().describe("File path (for read/symbols)"),
|
|
1220
|
+
pattern: z.string().optional().describe("Search pattern (for search)"),
|
|
1221
|
+
summarize: z.boolean().optional().describe("AI summarize (for read)"),
|
|
1222
|
+
format: z.enum(["raw", "summary"]).optional().describe("Output format (for execute)"),
|
|
1223
|
+
})).describe("Array of operations to run"),
|
|
1224
|
+
cwd: z.string().optional().describe("Working directory for all ops"),
|
|
1225
|
+
}, async ({ ops, cwd }) => {
|
|
1226
|
+
const start = Date.now();
|
|
1227
|
+
const workDir = cwd ?? process.cwd();
|
|
1228
|
+
const results = [];
|
|
1229
|
+
for (let i = 0; i < ops.slice(0, 10).length; i++) {
|
|
1230
|
+
const op = ops[i];
|
|
1231
|
+
try {
|
|
1232
|
+
if (op.type === "execute" && op.command) {
|
|
1233
|
+
const result = await exec(op.command, workDir, 30000);
|
|
1234
|
+
const output = (result.stdout + result.stderr).trim();
|
|
1235
|
+
if (op.format === "summary" && output.split("\n").length > 15) {
|
|
1236
|
+
const processed = await processOutput(op.command, output);
|
|
1237
|
+
results.push({ op: i, type: "execute", summary: processed.summary, exitCode: result.exitCode, tokensSaved: processed.tokensSaved });
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
results.push({ op: i, type: "execute", output: stripAnsi(output).slice(0, 2000), exitCode: result.exitCode });
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
else if (op.type === "read" && op.path) {
|
|
1244
|
+
const filePath = resolvePath(op.path, workDir);
|
|
1245
|
+
const result = cachedRead(filePath, {});
|
|
1246
|
+
if (op.summarize && result.content.length > 500) {
|
|
1247
|
+
const provider = getOutputProvider();
|
|
1248
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
1249
|
+
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
1250
|
+
const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
|
|
1251
|
+
model: outputModel,
|
|
1252
|
+
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.`,
|
|
1253
|
+
maxTokens: 300, temperature: 0.2,
|
|
1254
|
+
});
|
|
1255
|
+
results.push({ op: i, type: "read", path: op.path, summary, lines: result.content.split("\n").length });
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
results.push({ op: i, type: "read", path: op.path, content: result.content, lines: result.content.split("\n").length });
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
else if (op.type === "search" && op.pattern) {
|
|
1262
|
+
const result = await searchContent(op.pattern, op.path ? resolvePath(op.path, workDir) : workDir, {});
|
|
1263
|
+
results.push({ op: i, type: "search", pattern: op.pattern, totalMatches: result.totalMatches, files: result.files.slice(0, 10) });
|
|
1264
|
+
}
|
|
1265
|
+
else if (op.type === "symbols" && op.path) {
|
|
1266
|
+
const filePath = resolvePath(op.path, workDir);
|
|
1267
|
+
const result = cachedRead(filePath, {});
|
|
1268
|
+
if (result.content && !result.content.startsWith("Error:")) {
|
|
1269
|
+
const provider = getOutputProvider();
|
|
1270
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
1271
|
+
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
1272
|
+
const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
|
|
1273
|
+
model: outputModel,
|
|
1274
|
+
system: `Extract all symbols. Return ONLY a JSON array. Each: {"name":"x","kind":"function|class|method|interface|type","line":N,"signature":"brief"}. For class methods use "Class.method". Exclude imports.`,
|
|
1275
|
+
maxTokens: 2000, temperature: 0,
|
|
1276
|
+
});
|
|
1277
|
+
let symbols = [];
|
|
1278
|
+
try {
|
|
1279
|
+
const m = summary.match(/\[[\s\S]*\]/);
|
|
1280
|
+
if (m)
|
|
1281
|
+
symbols = JSON.parse(m[0]);
|
|
1282
|
+
}
|
|
1283
|
+
catch { }
|
|
1284
|
+
results.push({ op: i, type: "symbols", path: op.path, symbols });
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
results.push({ op: i, type: "symbols", path: op.path, error: "Cannot read file" });
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
catch (err) {
|
|
1292
|
+
results.push({ op: i, type: op.type, error: err.message?.slice(0, 200) });
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
logCall("batch", { command: `${ops.length} ops`, durationMs: Date.now() - start, aiProcessed: true });
|
|
1296
|
+
return { content: [{ type: "text", text: JSON.stringify({ results, total: results.length, durationMs: Date.now() - start }) }] };
|
|
1297
|
+
});
|
|
1211
1298
|
return server;
|
|
1212
1299
|
}
|
|
1213
1300
|
// ── main: start MCP server via stdio ─────────────────────────────────────────
|
package/package.json
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -698,22 +698,25 @@ export function createServer(): McpServer {
|
|
|
698
698
|
offset: z.number().optional().describe("Start line (0-indexed)"),
|
|
699
699
|
limit: z.number().optional().describe("Max lines to return"),
|
|
700
700
|
summarize: z.boolean().optional().describe("Return AI summary instead of full content (saves ~90% tokens)"),
|
|
701
|
+
focus: z.string().optional().describe("Focus hint for summary (e.g., 'public API', 'error handling', 'auth logic')"),
|
|
701
702
|
},
|
|
702
|
-
async ({ path: rawPath, offset, limit, summarize }) => {
|
|
703
|
+
async ({ path: rawPath, offset, limit, summarize, focus }) => {
|
|
703
704
|
const start = Date.now();
|
|
704
705
|
const path = resolvePath(rawPath);
|
|
705
706
|
const result = cachedRead(path, { offset, limit });
|
|
706
707
|
|
|
707
708
|
if (summarize && result.content.length > 500) {
|
|
708
|
-
// AI-native file summary — ask directly what the file does
|
|
709
709
|
const provider = getOutputProvider();
|
|
710
710
|
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
711
711
|
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
712
|
+
const focusInstruction = focus
|
|
713
|
+
? `Focus specifically on: ${focus}. Describe only aspects related to "${focus}".`
|
|
714
|
+
: `Describe what this source file does in 2-4 lines. Include: main class/module name, key methods/functions, what it exports, and its purpose.`;
|
|
712
715
|
const summary = await provider.complete(
|
|
713
716
|
`File: ${path}\n\n${content}`,
|
|
714
717
|
{
|
|
715
718
|
model: outputModel,
|
|
716
|
-
system:
|
|
719
|
+
system: `${focusInstruction} Be specific — name the actual functions and what they do. Never just say "N lines of code."`,
|
|
717
720
|
maxTokens: 300,
|
|
718
721
|
temperature: 0.2,
|
|
719
722
|
}
|
|
@@ -1570,6 +1573,87 @@ Be specific, not generic. Only flag real problems.`,
|
|
|
1570
1573
|
}
|
|
1571
1574
|
);
|
|
1572
1575
|
|
|
1576
|
+
// ── batch: multiple operations in one round trip ───────────────────────────
|
|
1577
|
+
|
|
1578
|
+
server.tool(
|
|
1579
|
+
"batch",
|
|
1580
|
+
"Run multiple operations in ONE call. Saves N-1 round trips. Each op can be: execute (run command), read (file read/summarize), search (grep pattern), or symbols (file outline).",
|
|
1581
|
+
{
|
|
1582
|
+
ops: z.array(z.object({
|
|
1583
|
+
type: z.enum(["execute", "read", "search", "symbols"]).describe("Operation type"),
|
|
1584
|
+
command: z.string().optional().describe("Shell command (for execute)"),
|
|
1585
|
+
path: z.string().optional().describe("File path (for read/symbols)"),
|
|
1586
|
+
pattern: z.string().optional().describe("Search pattern (for search)"),
|
|
1587
|
+
summarize: z.boolean().optional().describe("AI summarize (for read)"),
|
|
1588
|
+
format: z.enum(["raw", "summary"]).optional().describe("Output format (for execute)"),
|
|
1589
|
+
})).describe("Array of operations to run"),
|
|
1590
|
+
cwd: z.string().optional().describe("Working directory for all ops"),
|
|
1591
|
+
},
|
|
1592
|
+
async ({ ops, cwd }) => {
|
|
1593
|
+
const start = Date.now();
|
|
1594
|
+
const workDir = cwd ?? process.cwd();
|
|
1595
|
+
const results: Record<string, any>[] = [];
|
|
1596
|
+
|
|
1597
|
+
for (let i = 0; i < ops.slice(0, 10).length; i++) {
|
|
1598
|
+
const op = ops[i];
|
|
1599
|
+
try {
|
|
1600
|
+
if (op.type === "execute" && op.command) {
|
|
1601
|
+
const result = await exec(op.command, workDir, 30000);
|
|
1602
|
+
const output = (result.stdout + result.stderr).trim();
|
|
1603
|
+
if (op.format === "summary" && output.split("\n").length > 15) {
|
|
1604
|
+
const processed = await processOutput(op.command, output);
|
|
1605
|
+
results.push({ op: i, type: "execute", summary: processed.summary, exitCode: result.exitCode, tokensSaved: processed.tokensSaved });
|
|
1606
|
+
} else {
|
|
1607
|
+
results.push({ op: i, type: "execute", output: stripAnsi(output).slice(0, 2000), exitCode: result.exitCode });
|
|
1608
|
+
}
|
|
1609
|
+
} else if (op.type === "read" && op.path) {
|
|
1610
|
+
const filePath = resolvePath(op.path, workDir);
|
|
1611
|
+
const result = cachedRead(filePath, {});
|
|
1612
|
+
if (op.summarize && result.content.length > 500) {
|
|
1613
|
+
const provider = getOutputProvider();
|
|
1614
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
1615
|
+
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
1616
|
+
const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
|
|
1617
|
+
model: outputModel,
|
|
1618
|
+
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.`,
|
|
1619
|
+
maxTokens: 300, temperature: 0.2,
|
|
1620
|
+
});
|
|
1621
|
+
results.push({ op: i, type: "read", path: op.path, summary, lines: result.content.split("\n").length });
|
|
1622
|
+
} else {
|
|
1623
|
+
results.push({ op: i, type: "read", path: op.path, content: result.content, lines: result.content.split("\n").length });
|
|
1624
|
+
}
|
|
1625
|
+
} else if (op.type === "search" && op.pattern) {
|
|
1626
|
+
const result = await searchContent(op.pattern, op.path ? resolvePath(op.path, workDir) : workDir, {});
|
|
1627
|
+
results.push({ op: i, type: "search", pattern: op.pattern, totalMatches: result.totalMatches, files: result.files.slice(0, 10) });
|
|
1628
|
+
} else if (op.type === "symbols" && op.path) {
|
|
1629
|
+
const filePath = resolvePath(op.path, workDir);
|
|
1630
|
+
const result = cachedRead(filePath, {});
|
|
1631
|
+
if (result.content && !result.content.startsWith("Error:")) {
|
|
1632
|
+
const provider = getOutputProvider();
|
|
1633
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
1634
|
+
const content = result.content.length > 8000 ? result.content.slice(0, 8000) : result.content;
|
|
1635
|
+
const summary = await provider.complete(`File: ${filePath}\n\n${content}`, {
|
|
1636
|
+
model: outputModel,
|
|
1637
|
+
system: `Extract all symbols. Return ONLY a JSON array. Each: {"name":"x","kind":"function|class|method|interface|type","line":N,"signature":"brief"}. For class methods use "Class.method". Exclude imports.`,
|
|
1638
|
+
maxTokens: 2000, temperature: 0,
|
|
1639
|
+
});
|
|
1640
|
+
let symbols: any[] = [];
|
|
1641
|
+
try { const m = summary.match(/\[[\s\S]*\]/); if (m) symbols = JSON.parse(m[0]); } catch {}
|
|
1642
|
+
results.push({ op: i, type: "symbols", path: op.path, symbols });
|
|
1643
|
+
} else {
|
|
1644
|
+
results.push({ op: i, type: "symbols", path: op.path, error: "Cannot read file" });
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
} catch (err: any) {
|
|
1648
|
+
results.push({ op: i, type: op.type, error: err.message?.slice(0, 200) });
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
logCall("batch", { command: `${ops.length} ops`, durationMs: Date.now() - start, aiProcessed: true });
|
|
1653
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ results, total: results.length, durationMs: Date.now() - start }) }] };
|
|
1654
|
+
}
|
|
1655
|
+
);
|
|
1656
|
+
|
|
1573
1657
|
return server;
|
|
1574
1658
|
}
|
|
1575
1659
|
|