@hasna/terminal 3.7.3 → 3.8.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 +75 -1
- package/package.json +1 -1
- package/src/mcp/server.ts +92 -1
package/dist/mcp/server.js
CHANGED
|
@@ -436,7 +436,13 @@ export function createServer() {
|
|
|
436
436
|
// ── boot: session start context (replaces first 5 agent commands) ──────────
|
|
437
437
|
server.tool("boot", "Get everything an agent needs on session start in ONE call — git state, project info, source structure. Replaces: git status + git log + cat package.json + ls src/. Cached for the session.", async () => {
|
|
438
438
|
const ctx = await getBootContext(process.cwd());
|
|
439
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
439
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
440
|
+
...ctx,
|
|
441
|
+
hints: {
|
|
442
|
+
cwd: process.cwd(),
|
|
443
|
+
tip: "All terminal tools support relative paths. Use 'src/foo.ts' not the full absolute path. Use commit({message, push:true}) instead of raw git commands. Use run({task:'test'}) instead of bun/npm test. Use lookup({file, items}) instead of grep pipelines.",
|
|
444
|
+
},
|
|
445
|
+
}) }] };
|
|
440
446
|
});
|
|
441
447
|
// ── project_overview: orient agent in one call ─────────────────────────────
|
|
442
448
|
server.tool("project_overview", "Get project overview in one call — package.json info, source structure, config files. Replaces: cat package.json + ls src/ + cat tsconfig.json.", {
|
|
@@ -782,6 +788,74 @@ Match by function name, class name, method name (including ClassName.method), in
|
|
|
782
788
|
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }] };
|
|
783
789
|
}
|
|
784
790
|
});
|
|
791
|
+
server.tool("smart_commit", "AI-powered git commit. Analyzes all changes, groups into logical commits with generated messages, stages and commits each group, optionally pushes. One call replaces the entire git workflow. Agent just says 'commit my work'.", {
|
|
792
|
+
push: z.boolean().optional().describe("Push after all commits (default: true)"),
|
|
793
|
+
hint: z.string().optional().describe("Optional context about the changes (e.g., 'fixed auth + added users endpoint')"),
|
|
794
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
795
|
+
}, async ({ push, hint, cwd }) => {
|
|
796
|
+
const start = Date.now();
|
|
797
|
+
const workDir = cwd ?? process.cwd();
|
|
798
|
+
// 1. Get all changed files
|
|
799
|
+
const status = await exec("git status --porcelain", workDir, 10000);
|
|
800
|
+
const diffStat = await exec("git diff --stat", workDir, 10000);
|
|
801
|
+
const untrackedDiff = await exec("git diff HEAD --stat", workDir, 10000);
|
|
802
|
+
const changedFiles = status.stdout.trim();
|
|
803
|
+
if (!changedFiles) {
|
|
804
|
+
return { content: [{ type: "text", text: JSON.stringify({ message: "Nothing to commit — working tree clean" }) }] };
|
|
805
|
+
}
|
|
806
|
+
// 2. AI groups changes into logical commits
|
|
807
|
+
const provider = getOutputProvider();
|
|
808
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
809
|
+
const grouping = await provider.complete(`Changed files:\n${changedFiles}\n\nDiff stats:\n${diffStat.stdout}\n${untrackedDiff.stdout}${hint ? `\n\nContext: ${hint}` : ""}`, {
|
|
810
|
+
model: outputModel,
|
|
811
|
+
system: `You are a git commit assistant. Group these changed files into logical commits. Return ONLY a JSON array:
|
|
812
|
+
|
|
813
|
+
[{"message": "conventional commit message", "files": ["file1.ts", "file2.ts"]}]
|
|
814
|
+
|
|
815
|
+
Rules:
|
|
816
|
+
- Group related changes (same feature, same fix, same refactor)
|
|
817
|
+
- Use conventional commits: feat:, fix:, refactor:, test:, docs:, chore:
|
|
818
|
+
- Message should explain WHY, not WHAT (the diff shows what)
|
|
819
|
+
- Each file appears in exactly one group
|
|
820
|
+
- If all changes are related, use a single commit
|
|
821
|
+
- Extract file paths from the status output (skip the status prefix like M, A, ??)`,
|
|
822
|
+
maxTokens: 1000,
|
|
823
|
+
temperature: 0,
|
|
824
|
+
});
|
|
825
|
+
let commits = [];
|
|
826
|
+
try {
|
|
827
|
+
const jsonMatch = grouping.match(/\[[\s\S]*\]/);
|
|
828
|
+
if (jsonMatch)
|
|
829
|
+
commits = JSON.parse(jsonMatch[0]);
|
|
830
|
+
}
|
|
831
|
+
catch { }
|
|
832
|
+
if (commits.length === 0) {
|
|
833
|
+
// Fallback: single commit with all files
|
|
834
|
+
commits = [{ message: hint ?? "chore: update files", files: changedFiles.split("\n").map(l => l.slice(3).trim()) }];
|
|
835
|
+
}
|
|
836
|
+
// 3. Execute each commit
|
|
837
|
+
const results = [];
|
|
838
|
+
for (const c of commits) {
|
|
839
|
+
const fileArgs = c.files.map(f => `"${f}"`).join(" ");
|
|
840
|
+
const cmd = `git add ${fileArgs} && git commit -m ${JSON.stringify(c.message)}`;
|
|
841
|
+
const r = await exec(cmd, workDir, 15000);
|
|
842
|
+
results.push({ message: c.message, files: c.files.length, ok: r.exitCode === 0 });
|
|
843
|
+
}
|
|
844
|
+
// 4. Push if requested
|
|
845
|
+
let pushed = false;
|
|
846
|
+
if (push !== false) {
|
|
847
|
+
const pushResult = await exec("git push", workDir, 30000);
|
|
848
|
+
pushed = pushResult.exitCode === 0;
|
|
849
|
+
}
|
|
850
|
+
invalidateBootCache();
|
|
851
|
+
logCall("smart_commit", { command: `${commits.length} commits`, durationMs: Date.now() - start, aiProcessed: true });
|
|
852
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
853
|
+
commits: results,
|
|
854
|
+
pushed,
|
|
855
|
+
total: results.length,
|
|
856
|
+
ok: results.every(r => r.ok),
|
|
857
|
+
}) }] };
|
|
858
|
+
});
|
|
785
859
|
return server;
|
|
786
860
|
}
|
|
787
861
|
// ── main: start MCP server via stdio ─────────────────────────────────────────
|
package/package.json
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -615,7 +615,13 @@ export function createServer(): McpServer {
|
|
|
615
615
|
"Get everything an agent needs on session start in ONE call — git state, project info, source structure. Replaces: git status + git log + cat package.json + ls src/. Cached for the session.",
|
|
616
616
|
async () => {
|
|
617
617
|
const ctx = await getBootContext(process.cwd());
|
|
618
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(
|
|
618
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
619
|
+
...ctx,
|
|
620
|
+
hints: {
|
|
621
|
+
cwd: process.cwd(),
|
|
622
|
+
tip: "All terminal tools support relative paths. Use 'src/foo.ts' not the full absolute path. Use commit({message, push:true}) instead of raw git commands. Use run({task:'test'}) instead of bun/npm test. Use lookup({file, items}) instead of grep pipelines.",
|
|
623
|
+
},
|
|
624
|
+
}) }] };
|
|
619
625
|
}
|
|
620
626
|
);
|
|
621
627
|
|
|
@@ -1043,6 +1049,91 @@ Match by function name, class name, method name (including ClassName.method), in
|
|
|
1043
1049
|
}
|
|
1044
1050
|
);
|
|
1045
1051
|
|
|
1052
|
+
server.tool(
|
|
1053
|
+
"smart_commit",
|
|
1054
|
+
"AI-powered git commit. Analyzes all changes, groups into logical commits with generated messages, stages and commits each group, optionally pushes. One call replaces the entire git workflow. Agent just says 'commit my work'.",
|
|
1055
|
+
{
|
|
1056
|
+
push: z.boolean().optional().describe("Push after all commits (default: true)"),
|
|
1057
|
+
hint: z.string().optional().describe("Optional context about the changes (e.g., 'fixed auth + added users endpoint')"),
|
|
1058
|
+
cwd: z.string().optional().describe("Working directory"),
|
|
1059
|
+
},
|
|
1060
|
+
async ({ push, hint, cwd }) => {
|
|
1061
|
+
const start = Date.now();
|
|
1062
|
+
const workDir = cwd ?? process.cwd();
|
|
1063
|
+
|
|
1064
|
+
// 1. Get all changed files
|
|
1065
|
+
const status = await exec("git status --porcelain", workDir, 10000);
|
|
1066
|
+
const diffStat = await exec("git diff --stat", workDir, 10000);
|
|
1067
|
+
const untrackedDiff = await exec("git diff HEAD --stat", workDir, 10000);
|
|
1068
|
+
|
|
1069
|
+
const changedFiles = status.stdout.trim();
|
|
1070
|
+
if (!changedFiles) {
|
|
1071
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ message: "Nothing to commit — working tree clean" }) }] };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// 2. AI groups changes into logical commits
|
|
1075
|
+
const provider = getOutputProvider();
|
|
1076
|
+
const outputModel = provider.name === "groq" ? "llama-3.1-8b-instant" : undefined;
|
|
1077
|
+
|
|
1078
|
+
const grouping = await provider.complete(
|
|
1079
|
+
`Changed files:\n${changedFiles}\n\nDiff stats:\n${diffStat.stdout}\n${untrackedDiff.stdout}${hint ? `\n\nContext: ${hint}` : ""}`,
|
|
1080
|
+
{
|
|
1081
|
+
model: outputModel,
|
|
1082
|
+
system: `You are a git commit assistant. Group these changed files into logical commits. Return ONLY a JSON array:
|
|
1083
|
+
|
|
1084
|
+
[{"message": "conventional commit message", "files": ["file1.ts", "file2.ts"]}]
|
|
1085
|
+
|
|
1086
|
+
Rules:
|
|
1087
|
+
- Group related changes (same feature, same fix, same refactor)
|
|
1088
|
+
- Use conventional commits: feat:, fix:, refactor:, test:, docs:, chore:
|
|
1089
|
+
- Message should explain WHY, not WHAT (the diff shows what)
|
|
1090
|
+
- Each file appears in exactly one group
|
|
1091
|
+
- If all changes are related, use a single commit
|
|
1092
|
+
- Extract file paths from the status output (skip the status prefix like M, A, ??)`,
|
|
1093
|
+
maxTokens: 1000,
|
|
1094
|
+
temperature: 0,
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
let commits: { message: string; files: string[] }[] = [];
|
|
1099
|
+
try {
|
|
1100
|
+
const jsonMatch = grouping.match(/\[[\s\S]*\]/);
|
|
1101
|
+
if (jsonMatch) commits = JSON.parse(jsonMatch[0]);
|
|
1102
|
+
} catch {}
|
|
1103
|
+
|
|
1104
|
+
if (commits.length === 0) {
|
|
1105
|
+
// Fallback: single commit with all files
|
|
1106
|
+
commits = [{ message: hint ?? "chore: update files", files: changedFiles.split("\n").map(l => l.slice(3).trim()) }];
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// 3. Execute each commit
|
|
1110
|
+
const results: { message: string; files: number; ok: boolean }[] = [];
|
|
1111
|
+
for (const c of commits) {
|
|
1112
|
+
const fileArgs = c.files.map(f => `"${f}"`).join(" ");
|
|
1113
|
+
const cmd = `git add ${fileArgs} && git commit -m ${JSON.stringify(c.message)}`;
|
|
1114
|
+
const r = await exec(cmd, workDir, 15000);
|
|
1115
|
+
results.push({ message: c.message, files: c.files.length, ok: r.exitCode === 0 });
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// 4. Push if requested
|
|
1119
|
+
let pushed = false;
|
|
1120
|
+
if (push !== false) {
|
|
1121
|
+
const pushResult = await exec("git push", workDir, 30000);
|
|
1122
|
+
pushed = pushResult.exitCode === 0;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
invalidateBootCache();
|
|
1126
|
+
logCall("smart_commit", { command: `${commits.length} commits`, durationMs: Date.now() - start, aiProcessed: true });
|
|
1127
|
+
|
|
1128
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
1129
|
+
commits: results,
|
|
1130
|
+
pushed,
|
|
1131
|
+
total: results.length,
|
|
1132
|
+
ok: results.every(r => r.ok),
|
|
1133
|
+
}) }] };
|
|
1134
|
+
}
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1046
1137
|
return server;
|
|
1047
1138
|
}
|
|
1048
1139
|
|