@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.
@@ -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
- }, async ({ path: rawPath, offset, limit, summarize }) => {
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: `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."`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
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
@@ -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: `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."`,
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