@hasna/terminal 3.7.4 → 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 +68 -0
- package/package.json +1 -1
- package/src/mcp/server.ts +85 -0
package/dist/mcp/server.js
CHANGED
|
@@ -788,6 +788,74 @@ Match by function name, class name, method name (including ClassName.method), in
|
|
|
788
788
|
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }] };
|
|
789
789
|
}
|
|
790
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
|
+
});
|
|
791
859
|
return server;
|
|
792
860
|
}
|
|
793
861
|
// ── main: start MCP server via stdio ─────────────────────────────────────────
|
package/package.json
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -1049,6 +1049,91 @@ Match by function name, class name, method name (including ClassName.method), in
|
|
|
1049
1049
|
}
|
|
1050
1050
|
);
|
|
1051
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
|
+
|
|
1052
1137
|
return server;
|
|
1053
1138
|
}
|
|
1054
1139
|
|