internaltool-mcp 1.6.0 → 1.6.2

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 +236 -84
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -896,11 +896,16 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
896
896
  if (commitsRes?.success) recentCommits = commitsRes.data.commits || []
897
897
  } catch { /* GitHub may not be linked */ }
898
898
 
899
+ // If the task already has a branch linked, it's safe to move to in_progress now.
900
+ // If not, create_branch will do the move once the branch is created.
901
+ const alreadyHasBranch = !!task.github?.headBranch
899
902
  let moved = false
900
- try {
901
- const moveRes = await api.post(`/api/tasks/${taskId}/move`, { column: 'in_progress', toIndex: 0 })
902
- moved = moveRes?.success ?? false
903
- } catch { /* might already be in_progress */ }
903
+ if (alreadyHasBranch) {
904
+ try {
905
+ const moveRes = await api.post(`/api/tasks/${taskId}/move`, { column: 'in_progress', toIndex: 0 })
906
+ moved = moveRes?.success ?? false
907
+ } catch { /* might already be in_progress */ }
908
+ }
904
909
 
905
910
  return text({
906
911
  started: {
@@ -908,6 +913,7 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
908
913
  title: task.title,
909
914
  priority: task.priority,
910
915
  column: moved ? 'in_progress' : task.column,
916
+ branch: task.github?.headBranch || null,
911
917
  subtasks,
912
918
  },
913
919
  implementationPlan: hasReadme ? task.readmeMarkdown : null,
@@ -918,8 +924,10 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
918
924
  date: c.commit?.author?.date,
919
925
  })),
920
926
  movedToInProgress: moved,
921
- suggestedBranch,
922
- nextStep: `Use create_branch to create "${suggestedBranch}" on GitHub — it will automatically check your local git state before creating.`,
927
+ suggestedBranch: alreadyHasBranch ? null : suggestedBranch,
928
+ nextStep: alreadyHasBranch
929
+ ? `Branch "${task.github.headBranch}" already exists. Task is now In progress — start coding.`
930
+ : `Call create_branch to create "${suggestedBranch}" — it will check your local git state and move the task to In progress automatically.`,
923
931
  })
924
932
  }
925
933
  )
@@ -1530,6 +1538,17 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
1530
1538
  await api.patch(`/api/tasks/${taskId}/github/branch`, { headBranch: branchName })
1531
1539
  } catch { /* non-fatal — branch was still created on GitHub */ }
1532
1540
 
1541
+ // Move task to in_progress now that the branch exists (server guard now allows it)
1542
+ let movedToInProgress = false
1543
+ try {
1544
+ const freshTask = await api.get(`/api/tasks/${taskId}`)
1545
+ const col = freshTask?.data?.task?.column
1546
+ if (col && ['todo', 'backlog'].includes(col)) {
1547
+ const moveRes = await api.post(`/api/tasks/${taskId}/move`, { column: 'in_progress', toIndex: 0 })
1548
+ movedToInProgress = moveRes?.success ?? false
1549
+ }
1550
+ } catch { /* non-fatal */ }
1551
+
1533
1552
  const checkoutSteps = [
1534
1553
  'git fetch origin',
1535
1554
  `git checkout ${branchName}`,
@@ -1545,12 +1564,13 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
1545
1564
 
1546
1565
  return text({
1547
1566
  branchName,
1548
- url: res.data.url,
1567
+ url: res.data.url,
1549
1568
  localState,
1550
- message: `Branch "${branchName}" created on GitHub.`,
1551
- gitSteps: checkoutSteps,
1569
+ movedToInProgress,
1570
+ message: `Branch "${branchName}" created on GitHub.${movedToInProgress ? ' Task moved to In progress.' : ''}`,
1571
+ gitSteps: checkoutSteps,
1552
1572
  localStateNote,
1553
- nextStep: 'Run the git steps above to switch locally, then start coding. When commits are pushed, use raise_pr.',
1573
+ nextStep: 'Run the git steps above to switch locally, then start coding. When commits are pushed, use raise_pr.',
1554
1574
  })
1555
1575
  } catch (e) {
1556
1576
  return errorText(e.message)
@@ -1851,25 +1871,42 @@ Set confirmed=false first to preview the full PR content, then confirmed=true to
1851
1871
  // ── commit_helper ─────────────────────────────────────────────────────────────
1852
1872
  server.tool(
1853
1873
  'commit_helper',
1854
- `Analyse local git changes and suggest a ready-to-run conventional commit command.
1874
+ `Analyse local git changes and produce a ready-to-run conventional commit command.
1875
+
1876
+ Auto-detects local branch, changed files, and — when taskId is given — the task's linked branch.
1877
+ If the current branch does not match the task's branch, presents three options before doing anything:
1878
+ A) Switch to the task branch first, bring changes with you, then commit
1879
+ B) Stash changes now and commit on the task branch later
1880
+ C) Commit on the current branch (warns if that is main/master/dev)
1855
1881
 
1856
- Reads git diff --stat and git status locally — no 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
1882
+ Commit message format: <type>(<scope>): <description>
1883
+ type = feat | fix | refactor | chore | docs | test | style (auto-inferred)
1884
+ scope = task key (e.g. task-003) when taskId is provided
1860
1885
 
1861
- Set confirmed=false to preview the suggested message, confirmed=true to get the final copy-paste command.
1886
+ Also flags unsafe patterns before generating any command:
1887
+ - Untracked IDE/config dirs (.cursor/, .idea/, .vscode/, node_modules/)
1888
+ - Committing directly to main / master / dev without a task branch
1889
+
1890
+ TWO-STEP FLOW:
1891
+ confirmed=false → detect state, show branch mismatch options or commit preview
1892
+ confirmed=true + branchAction → execute the chosen path
1862
1893
 
1863
- Use this when the developer asks "help me commit", "what should my commit message be", or "how do I commit this".`,
1894
+ branchAction values (only needed when current branch task branch):
1895
+ "switch_then_commit" — stash → checkout task branch → pop stash → commit + push
1896
+ "stash_for_later" — stash only; commit later when on the task branch
1897
+ "commit_here" — commit on current branch (use only when changes truly belong here)`,
1864
1898
  {
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'),
1899
+ taskId: z.string().optional().describe("Task's MongoDB ObjectId"),
1900
+ repoPath: z.string().optional().describe('Absolute path to the local git repo'),
1901
+ confirmed: z.boolean().optional().default(false),
1902
+ branchAction: z.enum(['switch_then_commit', 'stash_for_later', 'commit_here'])
1903
+ .optional()
1904
+ .describe('Required when confirmed=true and current branch ≠ task branch'),
1868
1905
  },
1869
- async ({ taskId, repoPath, confirmed = false }) => {
1906
+ async ({ taskId, repoPath, confirmed = false, branchAction }) => {
1870
1907
  const cwd = repoPath || process.cwd()
1871
1908
 
1872
- // Read local git state
1909
+ // ── Read local git state ──────────────────────────────────────────────────
1873
1910
  let porcelain = '', diffStat = '', currentBranch = ''
1874
1911
  try {
1875
1912
  porcelain = runGit('status --porcelain=v1', cwd)
@@ -1885,7 +1922,7 @@ Use this when the developer asks "help me commit", "what should my commit messag
1885
1922
  return text({ message: 'Nothing to commit — working tree is clean.', currentBranch })
1886
1923
  }
1887
1924
 
1888
- // Fetch task context for scope/type hints
1925
+ // ── Fetch task ────────────────────────────────────────────────────────────
1889
1926
  let task = null
1890
1927
  if (taskId) {
1891
1928
  try {
@@ -1894,87 +1931,202 @@ Use this when the developer asks "help me commit", "what should my commit messag
1894
1931
  } catch { /* non-fatal */ }
1895
1932
  }
1896
1933
 
1897
- // Infer commit type from branch name + diff content
1898
- const branchLower = currentBranch.toLowerCase()
1899
- const diffLower = diffStat.toLowerCase()
1934
+ const taskBranch = task?.github?.headBranch || null
1935
+ const PROTECTED = ['main', 'master', 'dev', 'develop', 'staging', 'production']
1936
+ const onProtected = PROTECTED.includes(currentBranch)
1937
+ const branchMismatch = taskBranch && currentBranch !== taskBranch
1938
+
1939
+ // ── Detect unsafe untracked paths ─────────────────────────────────────────
1940
+ const NOCOMMIT_PATTERNS = ['.cursor', '.idea', '.vscode', 'node_modules', '.env', 'dist', 'build', '.DS_Store']
1941
+ const unsafeUntracked = untracked.filter(f =>
1942
+ NOCOMMIT_PATTERNS.some(p => f === p || f.startsWith(p + '/'))
1943
+ )
1944
+ const safeUntracked = untracked.filter(f =>
1945
+ !NOCOMMIT_PATTERNS.some(p => f === p || f.startsWith(p + '/'))
1946
+ )
1947
+
1948
+ // ── Build commit message ──────────────────────────────────────────────────
1949
+ // Use task branch name for type inference if available (more reliable than current branch)
1950
+ const branchForType = taskBranch || currentBranch
1951
+ const branchLower = branchForType.toLowerCase()
1952
+ const diffLower = diffStat.toLowerCase()
1900
1953
  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'
1954
+ if (branchLower.startsWith('feature/') || branchLower.startsWith('feat/')) commitType = 'feat'
1955
+ else if (branchLower.startsWith('fix/') || branchLower.startsWith('hotfix/')) commitType = 'fix'
1956
+ else if (branchLower.startsWith('refactor/')) commitType = 'refactor'
1957
+ else if (branchLower.startsWith('docs/')) commitType = 'docs'
1958
+ else if (branchLower.startsWith('test/')) commitType = 'test'
1959
+ else if (diffLower.includes('test') || diffLower.includes('spec')) commitType = 'test'
1908
1960
  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'
1961
+ const t = (task.title || '').toLowerCase()
1962
+ if (/\b(fix|bug|hotfix|patch)\b/.test(t)) commitType = 'fix'
1963
+ else if (/\b(refactor|cleanup)\b/.test(t)) commitType = 'refactor'
1964
+ else commitType = 'feat'
1913
1965
  }
1914
1966
 
1915
- // Build scope
1916
1967
  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}`),
1968
+ let description = task
1969
+ ? task.title.toLowerCase().replace(/[^a-z0-9 ]+/g, '').replace(/\s+/g, ' ').trim().slice(0, 60)
1970
+ : modified.length === 1
1971
+ ? `update ${modified[0].split('/').pop()}`
1972
+ : `update ${modified.length} file(s)`
1973
+
1974
+ const commitMsg = scope ? `${commitType}(${scope}): ${description}` : `${commitType}: ${description}`
1975
+
1976
+ // ── Build add command — exclude unsafe untracked ──────────────────────────
1977
+ const trackedFiles = [...modified, ...safeUntracked]
1978
+ const addCmd = unsafeUntracked.length > 0 && (unstaged.length > 0 || safeUntracked.length > 0)
1979
+ ? trackedFiles.map(f => `git add "${f}"`).join('\n') // explicit adds skip unsafe
1980
+ : unstaged.length > 0 || safeUntracked.length > 0
1981
+ ? 'git add .'
1982
+ : null // everything already staged
1983
+
1984
+ const changedFilesList = [
1985
+ ...staged.map(f => `staged: ${f.file}`),
1986
+ ...unstaged.map(f => `unstaged: ${f.file}`),
1987
+ ...safeUntracked.map(f => `untracked: ${f}`),
1988
+ ...unsafeUntracked.map(f => `⚠️ SKIP: ${f} do not commit this`),
1952
1989
  ]
1953
1990
 
1991
+ // ── PREVIEW (confirmed=false) ─────────────────────────────────────────────
1954
1992
  if (!confirmed) {
1993
+ // Case 1: branch mismatch — must resolve before anything else
1994
+ if (branchMismatch) {
1995
+ const stashMsg = `wip: ${scope || currentBranch} — switching to ${taskBranch}`
1996
+ return text({
1997
+ situation: {
1998
+ currentBranch,
1999
+ taskBranch,
2000
+ message: `You are on "${currentBranch}" but TASK-${task?.key} is linked to "${taskBranch}". Choose how to handle your local changes before committing.`,
2001
+ },
2002
+ changedFiles: changedFilesList,
2003
+ unsafeUntrackedWarning: unsafeUntracked.length
2004
+ ? `These paths should NOT be committed — add them to .gitignore: ${unsafeUntracked.join(', ')}`
2005
+ : null,
2006
+ options: {
2007
+ A: {
2008
+ branchAction: 'switch_then_commit',
2009
+ description: 'Switch to the task branch now, bring your changes, then commit (recommended)',
2010
+ commands: [
2011
+ `git stash push -m "${stashMsg}"`,
2012
+ `git fetch origin`,
2013
+ `git checkout ${taskBranch}`,
2014
+ `git stash pop`,
2015
+ addCmd,
2016
+ `git commit -m "${commitMsg}"`,
2017
+ `git push origin ${taskBranch}`,
2018
+ ].filter(Boolean),
2019
+ },
2020
+ B: {
2021
+ branchAction: 'stash_for_later',
2022
+ description: 'Stash changes now and commit on the task branch later',
2023
+ commands: [
2024
+ `git stash push -m "${stashMsg}"`,
2025
+ `# Later: git checkout ${taskBranch} && git stash pop`,
2026
+ ],
2027
+ },
2028
+ C: {
2029
+ branchAction: 'commit_here',
2030
+ description: `Commit on "${currentBranch}" as-is${onProtected ? ' ⚠️ THIS IS A PROTECTED BRANCH' : ''}`,
2031
+ warning: onProtected
2032
+ ? `"${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.`
2033
+ : null,
2034
+ commands: [addCmd, `git commit -m "${commitMsg}"`, `git push origin ${currentBranch}`].filter(Boolean),
2035
+ },
2036
+ },
2037
+ requiresConfirmation: true,
2038
+ message: `Call commit_helper again with confirmed=true and branchAction set to "switch_then_commit", "stash_for_later", or "commit_here".`,
2039
+ })
2040
+ }
2041
+
2042
+ // Case 2: on correct branch — standard preview
2043
+ const pushCmd = `git push origin ${currentBranch}`
1955
2044
  return text({
1956
2045
  preview: {
1957
2046
  suggestedMessage: commitMsg,
1958
- type: commitType,
2047
+ type: commitType,
1959
2048
  scope,
1960
- description,
1961
2049
  currentBranch,
1962
- changedFiles,
1963
- diffSummary: diffStat || '(no tracked changes yet)',
2050
+ taskBranch: taskBranch || '(none linked)',
2051
+ onCorrectBranch: !branchMismatch,
2052
+ changedFiles: changedFilesList,
1964
2053
  },
1965
- commands: [addCmd, commitCmd, pushCmd].filter(Boolean),
2054
+ unsafeUntrackedWarning: unsafeUntracked.length
2055
+ ? `These paths should NOT be committed — add them to .gitignore first: ${unsafeUntracked.join(', ')}`
2056
+ : null,
2057
+ protectedBranchWarning: onProtected
2058
+ ? `⚠️ You are on "${currentBranch}" — a protected branch. Consider committing on a feature branch instead.`
2059
+ : null,
2060
+ commands: [addCmd, `git commit -m "${commitMsg}"`, pushCmd].filter(Boolean),
1966
2061
  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.',
2062
+ message: `Suggested: "${commitMsg}". Call commit_helper again with confirmed=true to get the final commands.`,
2063
+ })
2064
+ }
2065
+
2066
+ // ── CONFIRMED (confirmed=true) ────────────────────────────────────────────
2067
+ if (branchMismatch && !branchAction) {
2068
+ return text({
2069
+ blocked: true,
2070
+ 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.`,
2071
+ })
2072
+ }
2073
+
2074
+ const stashMsg = `wip: ${scope || currentBranch} — switching to ${taskBranch}`
2075
+
2076
+ if (branchAction === 'switch_then_commit') {
2077
+ return text({
2078
+ plan: 'switch_then_commit',
2079
+ commands: [
2080
+ `git stash push -m "${stashMsg}"`,
2081
+ `git fetch origin`,
2082
+ `git checkout ${taskBranch}`,
2083
+ `git stash pop`,
2084
+ addCmd,
2085
+ `git commit -m "${commitMsg}"`,
2086
+ `git push origin ${taskBranch}`,
2087
+ ].filter(Boolean),
2088
+ commitMessage: commitMsg,
2089
+ message: `Run these commands in order. Your changes will land on "${taskBranch}" where they belong.`,
2090
+ nextStep: `After pushing, call raise_pr to open the pull request for ${task?.key}.`,
2091
+ unsafeUntrackedWarning: unsafeUntracked.length
2092
+ ? `These will be in your working tree after stash pop — do NOT git add them: ${unsafeUntracked.join(', ')}`
2093
+ : null,
2094
+ })
2095
+ }
2096
+
2097
+ if (branchAction === 'stash_for_later') {
2098
+ return text({
2099
+ plan: 'stash_for_later',
2100
+ commands: [
2101
+ `git stash push -m "${stashMsg}"`,
2102
+ ],
2103
+ message: `Your changes are stashed. When you're ready to commit:`,
2104
+ resumeCommands: [
2105
+ `git checkout ${taskBranch}`,
2106
+ `git stash pop`,
2107
+ addCmd,
2108
+ `git commit -m "${commitMsg}"`,
2109
+ `git push origin ${taskBranch}`,
2110
+ ].filter(Boolean),
2111
+ commitMessage: commitMsg,
1969
2112
  })
1970
2113
  }
1971
2114
 
2115
+ // commit_here (or no mismatch)
2116
+ const targetBranch = currentBranch
2117
+ const pushCmd = `git push origin ${targetBranch}`
1972
2118
  return text({
1973
2119
  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.',
2120
+ commands: [addCmd, `git commit -m "${commitMsg}"`, pushCmd].filter(Boolean),
2121
+ changedFiles: changedFilesList,
2122
+ protectedBranchWarning: onProtected
2123
+ ? `⚠️ Committing directly to "${targetBranch}". Only appropriate for base repo changes that don't belong to a feature branch.`
2124
+ : null,
2125
+ unsafeUntrackedWarning: unsafeUntracked.length
2126
+ ? `These were excluded from git add — add them to .gitignore: ${unsafeUntracked.join(', ')}`
2127
+ : null,
2128
+ message: `Copy-paste these commands in order.`,
2129
+ nextStep: taskBranch && !branchMismatch ? `After pushing, call raise_pr to open the pull request.` : null,
1978
2130
  })
1979
2131
  }
1980
2132
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "internaltool-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
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",