internaltool-mcp 1.6.0 → 1.6.1

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.
Files changed (2) hide show
  1. package/index.js +206 -74
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1851,25 +1851,42 @@ Set confirmed=false first to preview the full PR content, then confirmed=true to
1851
1851
  // ── commit_helper ─────────────────────────────────────────────────────────────
1852
1852
  server.tool(
1853
1853
  'commit_helper',
1854
- `Analyse local git changes and suggest a ready-to-run conventional commit command.
1854
+ `Analyse local git changes and produce a ready-to-run conventional commit command.
1855
1855
 
1856
- Reads git diff --stat and git status locallyno manual commands needed.
1857
- Generates a commit message following: <type>(<scope>): <description>
1858
- type = feat | fix | refactor | chore | docs | test | style
1859
- scope = task key (e.g. TASK-003) when taskId is provided
1856
+ Auto-detects local branch, changed files, and when taskId is given the task's linked branch.
1857
+ If the current branch does not match the task's branch, presents three options before doing anything:
1858
+ A) Switch to the task branch first, bring changes with you, then commit
1859
+ B) Stash changes now and commit on the task branch later
1860
+ C) Commit on the current branch (warns if that is main/master/dev)
1860
1861
 
1861
- Set confirmed=false to preview the suggested message, confirmed=true to get the final copy-paste command.
1862
+ Commit message format: <type>(<scope>): <description>
1863
+ type = feat | fix | refactor | chore | docs | test | style (auto-inferred)
1864
+ scope = task key (e.g. task-003) when taskId is provided
1862
1865
 
1863
- Use this when the developer asks "help me commit", "what should my commit message be", or "how do I commit this".`,
1866
+ Also flags unsafe patterns before generating any command:
1867
+ - Untracked IDE/config dirs (.cursor/, .idea/, .vscode/, node_modules/)
1868
+ - Committing directly to main / master / dev without a task branch
1869
+
1870
+ TWO-STEP FLOW:
1871
+ confirmed=false → detect state, show branch mismatch options or commit preview
1872
+ confirmed=true + branchAction → execute the chosen path
1873
+
1874
+ branchAction values (only needed when current branch ≠ task branch):
1875
+ "switch_then_commit" — stash → checkout task branch → pop stash → commit + push
1876
+ "stash_for_later" — stash only; commit later when on the task branch
1877
+ "commit_here" — commit on current branch (use only when changes truly belong here)`,
1864
1878
  {
1865
- taskId: z.string().optional().describe("Task's MongoDB ObjectId — used to set the commit scope to the task key"),
1866
- repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory)'),
1867
- confirmed: z.boolean().optional().default(false).describe('Set true to get the final runnable command'),
1879
+ taskId: z.string().optional().describe("Task's MongoDB ObjectId"),
1880
+ repoPath: z.string().optional().describe('Absolute path to the local git repo'),
1881
+ confirmed: z.boolean().optional().default(false),
1882
+ branchAction: z.enum(['switch_then_commit', 'stash_for_later', 'commit_here'])
1883
+ .optional()
1884
+ .describe('Required when confirmed=true and current branch ≠ task branch'),
1868
1885
  },
1869
- async ({ taskId, repoPath, confirmed = false }) => {
1886
+ async ({ taskId, repoPath, confirmed = false, branchAction }) => {
1870
1887
  const cwd = repoPath || process.cwd()
1871
1888
 
1872
- // Read local git state
1889
+ // ── Read local git state ──────────────────────────────────────────────────
1873
1890
  let porcelain = '', diffStat = '', currentBranch = ''
1874
1891
  try {
1875
1892
  porcelain = runGit('status --porcelain=v1', cwd)
@@ -1885,7 +1902,7 @@ Use this when the developer asks "help me commit", "what should my commit messag
1885
1902
  return text({ message: 'Nothing to commit — working tree is clean.', currentBranch })
1886
1903
  }
1887
1904
 
1888
- // Fetch task context for scope/type hints
1905
+ // ── Fetch task ────────────────────────────────────────────────────────────
1889
1906
  let task = null
1890
1907
  if (taskId) {
1891
1908
  try {
@@ -1894,87 +1911,202 @@ Use this when the developer asks "help me commit", "what should my commit messag
1894
1911
  } catch { /* non-fatal */ }
1895
1912
  }
1896
1913
 
1897
- // Infer commit type from branch name + diff content
1898
- const branchLower = currentBranch.toLowerCase()
1899
- const diffLower = diffStat.toLowerCase()
1914
+ const taskBranch = task?.github?.headBranch || null
1915
+ const PROTECTED = ['main', 'master', 'dev', 'develop', 'staging', 'production']
1916
+ const onProtected = PROTECTED.includes(currentBranch)
1917
+ const branchMismatch = taskBranch && currentBranch !== taskBranch
1918
+
1919
+ // ── Detect unsafe untracked paths ─────────────────────────────────────────
1920
+ const NOCOMMIT_PATTERNS = ['.cursor', '.idea', '.vscode', 'node_modules', '.env', 'dist', 'build', '.DS_Store']
1921
+ const unsafeUntracked = untracked.filter(f =>
1922
+ NOCOMMIT_PATTERNS.some(p => f === p || f.startsWith(p + '/'))
1923
+ )
1924
+ const safeUntracked = untracked.filter(f =>
1925
+ !NOCOMMIT_PATTERNS.some(p => f === p || f.startsWith(p + '/'))
1926
+ )
1927
+
1928
+ // ── Build commit message ──────────────────────────────────────────────────
1929
+ // Use task branch name for type inference if available (more reliable than current branch)
1930
+ const branchForType = taskBranch || currentBranch
1931
+ const branchLower = branchForType.toLowerCase()
1932
+ const diffLower = diffStat.toLowerCase()
1900
1933
  let commitType = 'chore'
1901
- if (branchLower.startsWith('feature/') || branchLower.startsWith('feat/')) commitType = 'feat'
1902
- else if (branchLower.startsWith('fix/') || branchLower.startsWith('hotfix/')) commitType = 'fix'
1903
- else if (branchLower.startsWith('refactor/')) commitType = 'refactor'
1904
- else if (branchLower.startsWith('docs/')) commitType = 'docs'
1905
- else if (branchLower.startsWith('test/')) commitType = 'test'
1906
- else if (diffLower.includes('test') || diffLower.includes('spec')) commitType = 'test'
1907
- else if (diffLower.includes('readme') || diffLower.includes('.md')) commitType = 'docs'
1934
+ if (branchLower.startsWith('feature/') || branchLower.startsWith('feat/')) commitType = 'feat'
1935
+ else if (branchLower.startsWith('fix/') || branchLower.startsWith('hotfix/')) commitType = 'fix'
1936
+ else if (branchLower.startsWith('refactor/')) commitType = 'refactor'
1937
+ else if (branchLower.startsWith('docs/')) commitType = 'docs'
1938
+ else if (branchLower.startsWith('test/')) commitType = 'test'
1939
+ else if (diffLower.includes('test') || diffLower.includes('spec')) commitType = 'test'
1908
1940
  else if (task) {
1909
- const titleLower = (task.title || '').toLowerCase()
1910
- if (/\b(fix|bug|hotfix|patch)\b/.test(titleLower)) commitType = 'fix'
1911
- else if (/\b(refactor|cleanup|clean up)\b/.test(titleLower)) commitType = 'refactor'
1912
- else commitType = 'feat'
1941
+ const t = (task.title || '').toLowerCase()
1942
+ if (/\b(fix|bug|hotfix|patch)\b/.test(t)) commitType = 'fix'
1943
+ else if (/\b(refactor|cleanup)\b/.test(t)) commitType = 'refactor'
1944
+ else commitType = 'feat'
1913
1945
  }
1914
1946
 
1915
- // Build scope
1916
1947
  const scope = task?.key?.toLowerCase() || ''
1917
-
1918
- // Build description from task title or changed files
1919
- let description = ''
1920
- if (task) {
1921
- description = task.title
1922
- .toLowerCase()
1923
- .replace(/[^a-z0-9 ]+/g, '')
1924
- .replace(/\s+/g, ' ')
1925
- .trim()
1926
- .slice(0, 60)
1927
- } else {
1928
- // Summarise from changed files
1929
- const allFiles = [...modified, ...untracked]
1930
- if (allFiles.length === 1) {
1931
- description = `update ${allFiles[0].split('/').pop()}`
1932
- } else {
1933
- description = `update ${allFiles.length} file(s)`
1934
- }
1935
- }
1936
-
1937
- const commitMsg = scope
1938
- ? `${commitType}(${scope}): ${description}`
1939
- : `${commitType}: ${description}`
1940
-
1941
- // Staged vs not staged — build the right add command
1942
- const hasUnstaged = unstaged.length > 0 || untracked.length > 0
1943
- const hasStaged = staged.length > 0
1944
- const addCmd = hasUnstaged ? 'git add .' : null
1945
- const commitCmd = `git commit -m "${commitMsg}"`
1946
- const pushCmd = `git push origin ${currentBranch}`
1947
-
1948
- const changedFiles = [
1949
- ...staged.map(f => ` staged: ${f.file}`),
1950
- ...unstaged.map(f => ` unstaged: ${f.file}`),
1951
- ...untracked.map(f => ` untracked: ${f}`),
1948
+ let description = task
1949
+ ? task.title.toLowerCase().replace(/[^a-z0-9 ]+/g, '').replace(/\s+/g, ' ').trim().slice(0, 60)
1950
+ : modified.length === 1
1951
+ ? `update ${modified[0].split('/').pop()}`
1952
+ : `update ${modified.length} file(s)`
1953
+
1954
+ const commitMsg = scope ? `${commitType}(${scope}): ${description}` : `${commitType}: ${description}`
1955
+
1956
+ // ── Build add command — exclude unsafe untracked ──────────────────────────
1957
+ const trackedFiles = [...modified, ...safeUntracked]
1958
+ const addCmd = unsafeUntracked.length > 0 && (unstaged.length > 0 || safeUntracked.length > 0)
1959
+ ? trackedFiles.map(f => `git add "${f}"`).join('\n') // explicit adds skip unsafe
1960
+ : unstaged.length > 0 || safeUntracked.length > 0
1961
+ ? 'git add .'
1962
+ : null // everything already staged
1963
+
1964
+ const changedFilesList = [
1965
+ ...staged.map(f => `staged: ${f.file}`),
1966
+ ...unstaged.map(f => `unstaged: ${f.file}`),
1967
+ ...safeUntracked.map(f => `untracked: ${f}`),
1968
+ ...unsafeUntracked.map(f => `⚠️ SKIP: ${f} do not commit this`),
1952
1969
  ]
1953
1970
 
1971
+ // ── PREVIEW (confirmed=false) ─────────────────────────────────────────────
1954
1972
  if (!confirmed) {
1973
+ // Case 1: branch mismatch — must resolve before anything else
1974
+ if (branchMismatch) {
1975
+ const stashMsg = `wip: ${scope || currentBranch} — switching to ${taskBranch}`
1976
+ return text({
1977
+ situation: {
1978
+ currentBranch,
1979
+ taskBranch,
1980
+ message: `You are on "${currentBranch}" but TASK-${task?.key} is linked to "${taskBranch}". Choose how to handle your local changes before committing.`,
1981
+ },
1982
+ changedFiles: changedFilesList,
1983
+ unsafeUntrackedWarning: unsafeUntracked.length
1984
+ ? `These paths should NOT be committed — add them to .gitignore: ${unsafeUntracked.join(', ')}`
1985
+ : null,
1986
+ options: {
1987
+ A: {
1988
+ branchAction: 'switch_then_commit',
1989
+ description: 'Switch to the task branch now, bring your changes, then commit (recommended)',
1990
+ commands: [
1991
+ `git stash push -m "${stashMsg}"`,
1992
+ `git fetch origin`,
1993
+ `git checkout ${taskBranch}`,
1994
+ `git stash pop`,
1995
+ addCmd,
1996
+ `git commit -m "${commitMsg}"`,
1997
+ `git push origin ${taskBranch}`,
1998
+ ].filter(Boolean),
1999
+ },
2000
+ B: {
2001
+ branchAction: 'stash_for_later',
2002
+ description: 'Stash changes now and commit on the task branch later',
2003
+ commands: [
2004
+ `git stash push -m "${stashMsg}"`,
2005
+ `# Later: git checkout ${taskBranch} && git stash pop`,
2006
+ ],
2007
+ },
2008
+ C: {
2009
+ branchAction: 'commit_here',
2010
+ description: `Commit on "${currentBranch}" as-is${onProtected ? ' ⚠️ THIS IS A PROTECTED BRANCH' : ''}`,
2011
+ warning: onProtected
2012
+ ? `"${currentBranch}" is a protected branch. Committing here directly bypasses the PR review process. Only do this for base repo changes (e.g. .gitignore, root README) that don't belong to any feature branch.`
2013
+ : null,
2014
+ commands: [addCmd, `git commit -m "${commitMsg}"`, `git push origin ${currentBranch}`].filter(Boolean),
2015
+ },
2016
+ },
2017
+ requiresConfirmation: true,
2018
+ message: `Call commit_helper again with confirmed=true and branchAction set to "switch_then_commit", "stash_for_later", or "commit_here".`,
2019
+ })
2020
+ }
2021
+
2022
+ // Case 2: on correct branch — standard preview
2023
+ const pushCmd = `git push origin ${currentBranch}`
1955
2024
  return text({
1956
2025
  preview: {
1957
2026
  suggestedMessage: commitMsg,
1958
- type: commitType,
2027
+ type: commitType,
1959
2028
  scope,
1960
- description,
1961
2029
  currentBranch,
1962
- changedFiles,
1963
- diffSummary: diffStat || '(no tracked changes yet)',
2030
+ taskBranch: taskBranch || '(none linked)',
2031
+ onCorrectBranch: !branchMismatch,
2032
+ changedFiles: changedFilesList,
1964
2033
  },
1965
- commands: [addCmd, commitCmd, pushCmd].filter(Boolean),
2034
+ unsafeUntrackedWarning: unsafeUntracked.length
2035
+ ? `These paths should NOT be committed — add them to .gitignore first: ${unsafeUntracked.join(', ')}`
2036
+ : null,
2037
+ protectedBranchWarning: onProtected
2038
+ ? `⚠️ You are on "${currentBranch}" — a protected branch. Consider committing on a feature branch instead.`
2039
+ : null,
2040
+ commands: [addCmd, `git commit -m "${commitMsg}"`, pushCmd].filter(Boolean),
1966
2041
  requiresConfirmation: true,
1967
- message: `Suggested commit message: "${commitMsg}". Call commit_helper again with confirmed=true to get the final runnable commands.`,
1968
- tip: 'You can edit the description before running — the type and scope are auto-detected but the description can be more specific.',
2042
+ message: `Suggested: "${commitMsg}". Call commit_helper again with confirmed=true to get the final commands.`,
2043
+ })
2044
+ }
2045
+
2046
+ // ── CONFIRMED (confirmed=true) ────────────────────────────────────────────
2047
+ if (branchMismatch && !branchAction) {
2048
+ return text({
2049
+ blocked: true,
2050
+ message: `Branch mismatch detected. You must set branchAction to one of: "switch_then_commit", "stash_for_later", "commit_here". Call commit_helper with confirmed=false first to see the options.`,
2051
+ })
2052
+ }
2053
+
2054
+ const stashMsg = `wip: ${scope || currentBranch} — switching to ${taskBranch}`
2055
+
2056
+ if (branchAction === 'switch_then_commit') {
2057
+ return text({
2058
+ plan: 'switch_then_commit',
2059
+ commands: [
2060
+ `git stash push -m "${stashMsg}"`,
2061
+ `git fetch origin`,
2062
+ `git checkout ${taskBranch}`,
2063
+ `git stash pop`,
2064
+ addCmd,
2065
+ `git commit -m "${commitMsg}"`,
2066
+ `git push origin ${taskBranch}`,
2067
+ ].filter(Boolean),
2068
+ commitMessage: commitMsg,
2069
+ message: `Run these commands in order. Your changes will land on "${taskBranch}" where they belong.`,
2070
+ nextStep: `After pushing, call raise_pr to open the pull request for ${task?.key}.`,
2071
+ unsafeUntrackedWarning: unsafeUntracked.length
2072
+ ? `These will be in your working tree after stash pop — do NOT git add them: ${unsafeUntracked.join(', ')}`
2073
+ : null,
2074
+ })
2075
+ }
2076
+
2077
+ if (branchAction === 'stash_for_later') {
2078
+ return text({
2079
+ plan: 'stash_for_later',
2080
+ commands: [
2081
+ `git stash push -m "${stashMsg}"`,
2082
+ ],
2083
+ message: `Your changes are stashed. When you're ready to commit:`,
2084
+ resumeCommands: [
2085
+ `git checkout ${taskBranch}`,
2086
+ `git stash pop`,
2087
+ addCmd,
2088
+ `git commit -m "${commitMsg}"`,
2089
+ `git push origin ${taskBranch}`,
2090
+ ].filter(Boolean),
2091
+ commitMessage: commitMsg,
1969
2092
  })
1970
2093
  }
1971
2094
 
2095
+ // commit_here (or no mismatch)
2096
+ const targetBranch = currentBranch
2097
+ const pushCmd = `git push origin ${targetBranch}`
1972
2098
  return text({
1973
2099
  commitMessage: commitMsg,
1974
- commands: [addCmd, commitCmd, pushCmd].filter(Boolean),
1975
- changedFiles,
1976
- message: `Copy-paste these commands in order. Edit the commit message description if needed before running.`,
1977
- nextStep: 'After pushing, call raise_pr to open the pull request.',
2100
+ commands: [addCmd, `git commit -m "${commitMsg}"`, pushCmd].filter(Boolean),
2101
+ changedFiles: changedFilesList,
2102
+ protectedBranchWarning: onProtected
2103
+ ? `⚠️ Committing directly to "${targetBranch}". Only appropriate for base repo changes that don't belong to a feature branch.`
2104
+ : null,
2105
+ unsafeUntrackedWarning: unsafeUntracked.length
2106
+ ? `These were excluded from git add — add them to .gitignore: ${unsafeUntracked.join(', ')}`
2107
+ : null,
2108
+ message: `Copy-paste these commands in order.`,
2109
+ nextStep: taskBranch && !branchMismatch ? `After pushing, call raise_pr to open the pull request.` : null,
1978
2110
  })
1979
2111
  }
1980
2112
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "internaltool-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "MCP server for InternalTool — connect AI assistants (Claude Code, Cursor) to your project and task management platform",
5
5
  "type": "module",
6
6
  "main": "index.js",