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