@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.
- package/dist/mcp/server.js +209 -3
- package/package.json +1 -1
- package/src/mcp/server.ts +231 -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
|
});
|
|
@@ -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
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
|
}
|
|
@@ -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
|
|