@hasna/terminal 4.0.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
  });
@@ -1089,6 +1092,209 @@ Be specific, not generic. Only flag real problems.`,
1089
1092
  }
1090
1093
  return { content: [{ type: "text", text: JSON.stringify({ notes, total: notes.length }) }] };
1091
1094
  });
1095
+ // ── diff: show what changed ────────────────────────────────────────────────
1096
+ server.tool("diff", "Show what changed — git diff with AI summary. One call replaces constructing git diff commands.", {
1097
+ ref: z.string().optional().describe("Diff against this ref (default: unstaged changes). Examples: HEAD~1, main, abc123"),
1098
+ file: z.string().optional().describe("Diff a specific file only"),
1099
+ stat: z.boolean().optional().describe("Show file-level stats only, not full diff (default: false)"),
1100
+ cwd: z.string().optional().describe("Working directory"),
1101
+ }, async ({ ref, file, stat, cwd }) => {
1102
+ const start = Date.now();
1103
+ const workDir = cwd ?? process.cwd();
1104
+ let cmd = "git diff";
1105
+ if (ref)
1106
+ cmd += ` ${ref}`;
1107
+ if (stat)
1108
+ cmd += " --stat";
1109
+ if (file)
1110
+ cmd += ` -- ${file}`;
1111
+ const result = await exec(cmd, workDir, 15000);
1112
+ const output = (result.stdout + result.stderr).trim();
1113
+ if (!output) {
1114
+ return { content: [{ type: "text", text: JSON.stringify({ clean: true, message: "No changes" }) }] };
1115
+ }
1116
+ const processed = await processOutput(cmd, output);
1117
+ logCall("diff", { command: cmd, outputTokens: estimateTokens(output), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, aiProcessed: processed.aiProcessed });
1118
+ return { content: [{ type: "text", text: JSON.stringify({
1119
+ summary: processed.summary,
1120
+ lines: output.split("\n").length,
1121
+ tokensSaved: processed.tokensSaved,
1122
+ }) }] };
1123
+ });
1124
+ // ── install: add packages, auto-detect package manager ────────────────────
1125
+ server.tool("install", "Install packages — auto-detects bun/npm/pnpm/yarn/pip/cargo. Agent says what to install, we figure out how.", {
1126
+ packages: z.array(z.string()).describe("Package names to install"),
1127
+ dev: z.boolean().optional().describe("Install as dev dependency (default: false)"),
1128
+ cwd: z.string().optional().describe("Working directory"),
1129
+ }, async ({ packages, dev, cwd }) => {
1130
+ const start = Date.now();
1131
+ const workDir = cwd ?? process.cwd();
1132
+ const { existsSync } = await import("fs");
1133
+ const { join } = await import("path");
1134
+ let cmd;
1135
+ const pkgs = packages.join(" ");
1136
+ const devFlag = dev ? " -D" : "";
1137
+ if (existsSync(join(workDir, "bun.lockb")) || existsSync(join(workDir, "bun.lock"))) {
1138
+ cmd = `bun add${devFlag} ${pkgs}`;
1139
+ }
1140
+ else if (existsSync(join(workDir, "pnpm-lock.yaml"))) {
1141
+ cmd = `pnpm add${devFlag} ${pkgs}`;
1142
+ }
1143
+ else if (existsSync(join(workDir, "yarn.lock"))) {
1144
+ cmd = `yarn add${dev ? " --dev" : ""} ${pkgs}`;
1145
+ }
1146
+ else if (existsSync(join(workDir, "package.json"))) {
1147
+ cmd = `npm install${dev ? " --save-dev" : ""} ${pkgs}`;
1148
+ }
1149
+ else if (existsSync(join(workDir, "requirements.txt")) || existsSync(join(workDir, "pyproject.toml"))) {
1150
+ cmd = `pip install ${pkgs}`;
1151
+ }
1152
+ else if (existsSync(join(workDir, "Cargo.toml"))) {
1153
+ cmd = `cargo add ${pkgs}`;
1154
+ }
1155
+ else {
1156
+ cmd = `npm install${dev ? " --save-dev" : ""} ${pkgs}`;
1157
+ }
1158
+ const result = await exec(cmd, workDir, 60000);
1159
+ const output = (result.stdout + result.stderr).trim();
1160
+ const processed = await processOutput(cmd, output);
1161
+ logCall("install", { command: cmd, exitCode: result.exitCode, durationMs: Date.now() - start, aiProcessed: processed.aiProcessed });
1162
+ return { content: [{ type: "text", text: JSON.stringify({
1163
+ exitCode: result.exitCode,
1164
+ command: cmd,
1165
+ summary: processed.summary,
1166
+ }) }] };
1167
+ });
1168
+ // ── help: tool discoverability ────────────────────────────────────────────
1169
+ server.tool("help", "Get recommendations for which terminal tool to use. Describe what you want to do and get the best tool + usage example.", {
1170
+ goal: z.string().optional().describe("What you're trying to do (e.g., 'run tests', 'find where login is defined', 'commit my changes')"),
1171
+ }, async ({ goal }) => {
1172
+ if (!goal) {
1173
+ return { content: [{ type: "text", text: JSON.stringify({
1174
+ tools: {
1175
+ "execute / execute_smart": "Run any command. Smart = AI summary (80% fewer tokens)",
1176
+ "run({task})": "Run test/build/lint — auto-detects toolchain",
1177
+ "commit / bulk_commit / smart_commit": "Git commit — single, multi, or AI-grouped",
1178
+ "diff({ref})": "Show what changed with AI summary",
1179
+ "install({packages})": "Add packages — auto-detects bun/npm/pip/cargo",
1180
+ "search_content({pattern})": "Grep with structured results",
1181
+ "search_files({pattern})": "Find files by glob",
1182
+ "symbols({path})": "AI file outline — any language",
1183
+ "read_symbol({path, name})": "Read one function/class by name",
1184
+ "read_file({path, summarize})": "Read or AI-summarize a file",
1185
+ "read_files({files, summarize})": "Multi-file read in one call",
1186
+ "symbols_dir({path})": "Symbols for entire directory",
1187
+ "review({since})": "AI code review",
1188
+ "lookup({file, items})": "Find items in a file by name",
1189
+ "edit({file, find, replace})": "Find-replace in file",
1190
+ "repo_state": "Git branch + status + log in one call",
1191
+ "boot": "Full project context on session start",
1192
+ "watch({task})": "Run task on file change",
1193
+ "store_secret / list_secrets": "Secrets vault",
1194
+ "project_note({save/recall})": "Persistent project notes",
1195
+ },
1196
+ tips: [
1197
+ "Use relative paths — 'src/foo.ts' not '/Users/.../src/foo.ts'",
1198
+ "Use your native Read/Write/Edit for file operations when you don't need AI summary",
1199
+ "Use search_content for text patterns, symbols for code structure",
1200
+ "Use commit for single, bulk_commit for multiple, smart_commit for AI-grouped",
1201
+ ],
1202
+ }) }] };
1203
+ }
1204
+ // AI recommends the best tool for the goal
1205
+ const provider = getOutputProvider();
1206
+ const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
1207
+ const recommendation = await provider.complete(`Agent wants to: ${goal}\n\nAvailable tools: execute, execute_smart, run, commit, bulk_commit, smart_commit, diff, install, search_content, search_files, symbols, read_symbol, read_file, read_files, symbols_dir, review, lookup, edit, repo_state, boot, watch, store_secret, list_secrets, project_note, help`, {
1208
+ model: outputModel,
1209
+ system: `Recommend the best terminal MCP tool for this goal. Return JSON: {"tool": "name", "example": {params}, "why": "one line"}. If multiple tools work, list top 2.`,
1210
+ maxTokens: 200, temperature: 0,
1211
+ });
1212
+ return { content: [{ type: "text", text: recommendation }] };
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
+ });
1092
1298
  return server;
1093
1299
  }
1094
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.0.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
  }
@@ -1426,6 +1429,231 @@ Be specific, not generic. Only flag real problems.`,
1426
1429
  }
1427
1430
  );
1428
1431
 
1432
+ // ── diff: show what changed ────────────────────────────────────────────────
1433
+
1434
+ server.tool(
1435
+ "diff",
1436
+ "Show what changed — git diff with AI summary. One call replaces constructing git diff commands.",
1437
+ {
1438
+ ref: z.string().optional().describe("Diff against this ref (default: unstaged changes). Examples: HEAD~1, main, abc123"),
1439
+ file: z.string().optional().describe("Diff a specific file only"),
1440
+ stat: z.boolean().optional().describe("Show file-level stats only, not full diff (default: false)"),
1441
+ cwd: z.string().optional().describe("Working directory"),
1442
+ },
1443
+ async ({ ref, file, stat, cwd }) => {
1444
+ const start = Date.now();
1445
+ const workDir = cwd ?? process.cwd();
1446
+ let cmd = "git diff";
1447
+ if (ref) cmd += ` ${ref}`;
1448
+ if (stat) cmd += " --stat";
1449
+ if (file) cmd += ` -- ${file}`;
1450
+
1451
+ const result = await exec(cmd, workDir, 15000);
1452
+ const output = (result.stdout + result.stderr).trim();
1453
+
1454
+ if (!output) {
1455
+ return { content: [{ type: "text" as const, text: JSON.stringify({ clean: true, message: "No changes" }) }] };
1456
+ }
1457
+
1458
+ const processed = await processOutput(cmd, output);
1459
+ logCall("diff", { command: cmd, outputTokens: estimateTokens(output), tokensSaved: processed.tokensSaved, durationMs: Date.now() - start, aiProcessed: processed.aiProcessed });
1460
+
1461
+ return { content: [{ type: "text" as const, text: JSON.stringify({
1462
+ summary: processed.summary,
1463
+ lines: output.split("\n").length,
1464
+ tokensSaved: processed.tokensSaved,
1465
+ }) }] };
1466
+ }
1467
+ );
1468
+
1469
+ // ── install: add packages, auto-detect package manager ────────────────────
1470
+
1471
+ server.tool(
1472
+ "install",
1473
+ "Install packages — auto-detects bun/npm/pnpm/yarn/pip/cargo. Agent says what to install, we figure out how.",
1474
+ {
1475
+ packages: z.array(z.string()).describe("Package names to install"),
1476
+ dev: z.boolean().optional().describe("Install as dev dependency (default: false)"),
1477
+ cwd: z.string().optional().describe("Working directory"),
1478
+ },
1479
+ async ({ packages, dev, cwd }) => {
1480
+ const start = Date.now();
1481
+ const workDir = cwd ?? process.cwd();
1482
+ const { existsSync } = await import("fs");
1483
+ const { join } = await import("path");
1484
+
1485
+ let cmd: string;
1486
+ const pkgs = packages.join(" ");
1487
+ const devFlag = dev ? " -D" : "";
1488
+
1489
+ if (existsSync(join(workDir, "bun.lockb")) || existsSync(join(workDir, "bun.lock"))) {
1490
+ cmd = `bun add${devFlag} ${pkgs}`;
1491
+ } else if (existsSync(join(workDir, "pnpm-lock.yaml"))) {
1492
+ cmd = `pnpm add${devFlag} ${pkgs}`;
1493
+ } else if (existsSync(join(workDir, "yarn.lock"))) {
1494
+ cmd = `yarn add${dev ? " --dev" : ""} ${pkgs}`;
1495
+ } else if (existsSync(join(workDir, "package.json"))) {
1496
+ cmd = `npm install${dev ? " --save-dev" : ""} ${pkgs}`;
1497
+ } else if (existsSync(join(workDir, "requirements.txt")) || existsSync(join(workDir, "pyproject.toml"))) {
1498
+ cmd = `pip install ${pkgs}`;
1499
+ } else if (existsSync(join(workDir, "Cargo.toml"))) {
1500
+ cmd = `cargo add ${pkgs}`;
1501
+ } else {
1502
+ cmd = `npm install${dev ? " --save-dev" : ""} ${pkgs}`;
1503
+ }
1504
+
1505
+ const result = await exec(cmd, workDir, 60000);
1506
+ const output = (result.stdout + result.stderr).trim();
1507
+ const processed = await processOutput(cmd, output);
1508
+ logCall("install", { command: cmd, exitCode: result.exitCode, durationMs: Date.now() - start, aiProcessed: processed.aiProcessed });
1509
+
1510
+ return { content: [{ type: "text" as const, text: JSON.stringify({
1511
+ exitCode: result.exitCode,
1512
+ command: cmd,
1513
+ summary: processed.summary,
1514
+ }) }] };
1515
+ }
1516
+ );
1517
+
1518
+ // ── help: tool discoverability ────────────────────────────────────────────
1519
+
1520
+ server.tool(
1521
+ "help",
1522
+ "Get recommendations for which terminal tool to use. Describe what you want to do and get the best tool + usage example.",
1523
+ {
1524
+ goal: z.string().optional().describe("What you're trying to do (e.g., 'run tests', 'find where login is defined', 'commit my changes')"),
1525
+ },
1526
+ async ({ goal }) => {
1527
+ if (!goal) {
1528
+ return { content: [{ type: "text" as const, text: JSON.stringify({
1529
+ tools: {
1530
+ "execute / execute_smart": "Run any command. Smart = AI summary (80% fewer tokens)",
1531
+ "run({task})": "Run test/build/lint — auto-detects toolchain",
1532
+ "commit / bulk_commit / smart_commit": "Git commit — single, multi, or AI-grouped",
1533
+ "diff({ref})": "Show what changed with AI summary",
1534
+ "install({packages})": "Add packages — auto-detects bun/npm/pip/cargo",
1535
+ "search_content({pattern})": "Grep with structured results",
1536
+ "search_files({pattern})": "Find files by glob",
1537
+ "symbols({path})": "AI file outline — any language",
1538
+ "read_symbol({path, name})": "Read one function/class by name",
1539
+ "read_file({path, summarize})": "Read or AI-summarize a file",
1540
+ "read_files({files, summarize})": "Multi-file read in one call",
1541
+ "symbols_dir({path})": "Symbols for entire directory",
1542
+ "review({since})": "AI code review",
1543
+ "lookup({file, items})": "Find items in a file by name",
1544
+ "edit({file, find, replace})": "Find-replace in file",
1545
+ "repo_state": "Git branch + status + log in one call",
1546
+ "boot": "Full project context on session start",
1547
+ "watch({task})": "Run task on file change",
1548
+ "store_secret / list_secrets": "Secrets vault",
1549
+ "project_note({save/recall})": "Persistent project notes",
1550
+ },
1551
+ tips: [
1552
+ "Use relative paths — 'src/foo.ts' not '/Users/.../src/foo.ts'",
1553
+ "Use your native Read/Write/Edit for file operations when you don't need AI summary",
1554
+ "Use search_content for text patterns, symbols for code structure",
1555
+ "Use commit for single, bulk_commit for multiple, smart_commit for AI-grouped",
1556
+ ],
1557
+ }) }] };
1558
+ }
1559
+
1560
+ // AI recommends the best tool for the goal
1561
+ const provider = getOutputProvider();
1562
+ const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
1563
+ const recommendation = await provider.complete(
1564
+ `Agent wants to: ${goal}\n\nAvailable tools: execute, execute_smart, run, commit, bulk_commit, smart_commit, diff, install, search_content, search_files, symbols, read_symbol, read_file, read_files, symbols_dir, review, lookup, edit, repo_state, boot, watch, store_secret, list_secrets, project_note, help`,
1565
+ {
1566
+ model: outputModel,
1567
+ system: `Recommend the best terminal MCP tool for this goal. Return JSON: {"tool": "name", "example": {params}, "why": "one line"}. If multiple tools work, list top 2.`,
1568
+ maxTokens: 200, temperature: 0,
1569
+ }
1570
+ );
1571
+
1572
+ return { content: [{ type: "text" as const, text: recommendation }] };
1573
+ }
1574
+ );
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
+
1429
1657
  return server;
1430
1658
  }
1431
1659