@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "3.7.4",
3
+ "version": "3.8.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
@@ -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