internaltool-mcp 1.6.14 → 1.6.17

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 +164 -62
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -302,13 +302,17 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
302
302
 
303
303
  server.tool(
304
304
  'park_task',
305
- `Pause a task and save your current work context so another developer can safely pick it up.
305
+ `Pause a task and hand it off safely so another developer can continue without losing any work.
306
306
 
307
- SAFETY REQUIREMENT: All local changes must be committed and pushed before parking.
308
- If there are uncommitted or unpushed changes, this tool will block and tell you to push first.
309
- This ensures no work is lost when someone else picks up the task.
307
+ WHAT THIS DOES AUTOMATICALLY (confirmed=true):
308
+ 1. Verifies all changes are committed and pushed blocks if not
309
+ 2. Auto-pushes any unpushed commits to remote
310
+ 3. Saves your park note (summary, remaining, blockers) to the task
311
+ 4. Deletes the cursor rules file (.cursor/rules/<task>.mdc) so it doesn't bleed into other tasks
312
+ 5. Posts a comment on the task with your handoff notes (visible to Dev B)
313
+ 6. Notifies all project members that this task is parked and ready for pickup
310
314
 
311
- Set confirmed=false first to preview, then confirmed=true to actually save.`,
315
+ Set confirmed=false first to preview, then confirmed=true to execute everything.`,
312
316
  {
313
317
  taskId: z.string().describe("Task's MongoDB ObjectId"),
314
318
  summary: z.string().optional().describe('What was built/changed — include file names and what was done'),
@@ -321,111 +325,190 @@ Set confirmed=false first to preview, then confirmed=true to actually save.`,
321
325
  const taskRes = await api.get(`/api/tasks/${taskId}`)
322
326
  const task = taskRes?.data?.task
323
327
 
324
- // ── Safety check: block if there are uncommitted or unpushed changes ──
325
- const cwd = repoPath || process.cwd()
328
+ const cwd = repoPath || process.cwd()
326
329
  const repoRoot = findRepoRoot(cwd)
327
- let gitBlockers = []
330
+
331
+ // ── Git safety checks ──────────────────────────────────────────────────
332
+ let uncommitted = false
333
+ let unpushedCount = 0
334
+ let currentBranch = ''
328
335
  if (repoRoot) {
329
336
  try {
330
337
  const porcelain = runGit('status --porcelain=v1', repoRoot)
331
- const { localState } = parseGitStatus(porcelain)
332
- if (localState === 'modified') {
333
- gitBlockers.push('You have uncommitted local changes. Commit them first so your work is not lost:\n git add . && git commit -m "<your message>" && git push')
334
- }
338
+ uncommitted = parseGitStatus(porcelain).localState === 'modified'
335
339
  } catch { /* non-fatal */ }
336
340
  try {
337
- const currentBranch = runGit('branch --show-current', repoRoot).trim()
341
+ currentBranch = runGit('branch --show-current', repoRoot).trim()
338
342
  if (currentBranch) {
339
- runGit('fetch origin', repoRoot)
340
- const unpushed = parseInt(runGit(`rev-list origin/${currentBranch}..HEAD --count`, repoRoot).trim(), 10) || 0
341
- if (unpushed > 0) {
342
- gitBlockers.push(`You have ${unpushed} unpushed commit(s) on "${currentBranch}". Push first so another developer can access them:\n git push origin ${currentBranch}`)
343
- }
343
+ try { runGit('fetch origin', repoRoot) } catch { /* no network */ }
344
+ unpushedCount = parseInt(runGit(`rev-list origin/${currentBranch}..HEAD --count`, repoRoot).trim(), 10) || 0
344
345
  }
345
- } catch { /* remote branch may not exist — treat as unpushed */ }
346
+ } catch { /* remote branch may not exist */ }
346
347
  }
347
348
 
348
- if (gitBlockers.length > 0) {
349
+ if (uncommitted) {
349
350
  return text({
350
351
  blocked: true,
351
- reason: 'Cannot park task changes are not fully committed and pushed.',
352
- gitBlockers,
353
- message: 'Fix the issues above, then call park_task again.',
352
+ reason: 'You have uncommitted local changes.',
353
+ fix: 'Commit your changes first, then call park_task again.',
354
+ commands: ['git add .', 'git commit -m "wip: parking task — <describe what you did>"'],
355
+ message: 'Cannot park until all changes are committed.',
354
356
  })
355
357
  }
356
358
 
357
359
  if (!confirmed) {
358
360
  return text({
359
361
  preview: {
360
- action: 'park_task',
361
- taskId,
362
362
  task: task ? { key: task.key, title: task.title, branch: task.github?.headBranch || null } : null,
363
- willSave: { summary: summary || '(empty)', remaining: remaining || '(empty)', blockers: blockers || '(empty)' },
364
- cursorRulesNote: task?.cursorRules?.trim() ? `Cursor rules file (.cursor/rules/${task.key?.toLowerCase()}.mdc) will be removed while parked.` : null,
365
- handoffNote: task?.github?.headBranch
366
- ? `All work is pushed. Another developer can pick this up by running: git fetch origin && git checkout ${task.github.headBranch}`
367
- : null,
363
+ parkNote: { summary: summary || '(empty)', remaining: remaining || '(empty)', blockers: blockers || '(empty)' },
364
+ willAutomate: [
365
+ unpushedCount > 0 ? `Push ${unpushedCount} unpushed commit(s) to remote` : 'Branch is already up to date on remote',
366
+ 'Save park note to task',
367
+ task?.cursorRules?.trim() ? 'Delete cursor rules file (.cursor/rules/' + task.key?.toLowerCase() + '.mdc)' : null,
368
+ 'Post handoff comment on the task',
369
+ 'Notify project members',
370
+ ].filter(Boolean),
368
371
  },
369
372
  requiresConfirmation: true,
370
- message: 'All changes are pushed. Review the park note above then call park_task again with confirmed=true.',
373
+ message: 'Review the preview above, then call park_task again with confirmed=true to execute.',
371
374
  })
372
375
  }
373
376
 
374
- const res = await api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers })
375
- // Remove cursor rules file while task is parked — another task may be active
377
+ // ── Auto-push unpushed commits ─────────────────────────────────────────
378
+ let pushed = false
379
+ if (repoRoot && currentBranch && unpushedCount > 0) {
380
+ try {
381
+ runGit(`push origin ${currentBranch}`, repoRoot)
382
+ pushed = true
383
+ } catch (e) {
384
+ return text({ blocked: true, reason: `Auto-push failed: ${e.message.split('\n')[0]}. Push manually then call park_task again.` })
385
+ }
386
+ }
387
+
388
+ // ── Save park note + server-side comment & notifications ──────────────
389
+ await api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers })
390
+
391
+ // ── Delete cursor rules file ───────────────────────────────────────────
376
392
  let cursorRulesCleared = null
377
393
  if (task?.cursorRules?.trim()) {
378
394
  cursorRulesCleared = deleteCursorRulesFile(task.key, repoPath)
379
395
  }
396
+
380
397
  return text({
381
- ...(res?.data || {}),
382
- cursorRulesCleared,
383
- handoff: task?.github?.headBranch
384
- ? { branch: task.github.headBranch, checkoutSteps: ['git fetch origin', `git checkout ${task.github.headBranch}`] }
385
- : null,
398
+ parked: true,
399
+ task: { key: task?.key, title: task?.title, branch: task?.github?.headBranch || null },
400
+ autoPushed: pushed ? `${unpushedCount} commit(s) pushed to origin/${currentBranch}` : 'No push needed',
401
+ cursorRulesCleared: cursorRulesCleared ? `Deleted ${cursorRulesCleared}` : null,
402
+ commentPosted: true,
403
+ teamNotified: true,
404
+ message: 'Task parked. Project members have been notified. Dev B can run unpark_task to continue.',
386
405
  })
387
406
  }
388
407
  )
389
408
 
390
409
  server.tool(
391
410
  'unpark_task',
392
- 'Resume a parked task shows exactly what the previous developer left behind and what to do next.',
411
+ `Pick up a parked task and get fully set up to continue the previous developer's work.
412
+
413
+ WHAT THIS DOES AUTOMATICALLY (confirmed=true):
414
+ 1. Shows the full park note from the previous developer before doing anything
415
+ 2. Shows the last 5 commits on the branch so you know the exact state of the code
416
+ 3. Shows recent task comments so you have full context
417
+ 4. Runs: git fetch origin + git checkout <branch> + git pull (switches your local repo)
418
+ 5. Restores the cursor rules file (.cursor/rules/<task>.mdc) so Cursor follows task rules
419
+ 6. Posts a comment that you picked up the task
420
+ 7. Notifies the previous developer that you have taken over
421
+
422
+ Set confirmed=false first to read everything, then confirmed=true to execute.`,
393
423
  {
394
424
  taskId: z.string().describe("Task's MongoDB ObjectId"),
395
- confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
396
- repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory). Used to restore cursor rules file.'),
425
+ confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the park note'),
426
+ repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory).'),
397
427
  },
398
428
  async ({ taskId, confirmed = false, repoPath }) => {
399
429
  const taskRes = await api.get(`/api/tasks/${taskId}`)
400
430
  const task = taskRes?.data?.task
401
431
  const branch = task?.github?.headBranch || null
432
+
433
+ // ── Fetch recent commits on the branch ────────────────────────────────
434
+ let recentCommits = []
435
+ if (branch) {
436
+ try {
437
+ const commitsRes = await api.get(`/api/projects/${task.project}/github/commits?per_page=5&branch=${branch}`)
438
+ recentCommits = commitsRes?.data?.commits || []
439
+ } catch { /* non-fatal */ }
440
+ }
441
+
442
+ // ── Fetch recent task comments ─────────────────────────────────────────
443
+ let recentComments = []
444
+ try {
445
+ const commentsRes = await api.get(`/api/tasks/${taskId}/comments`)
446
+ recentComments = (commentsRes?.data?.comments || []).slice(-5).map(c => ({
447
+ author: c.author?.name || c.author?.email || 'unknown',
448
+ body: c.body?.slice(0, 300),
449
+ at: c.createdAt,
450
+ }))
451
+ } catch { /* non-fatal */ }
452
+
402
453
  if (!confirmed) {
403
454
  return text({
404
- preview: {
405
- action: 'unpark_task',
406
- taskId,
407
- task: task ? { key: task.key, title: task.title, column: task.column } : null,
408
- parkNote: task?.parkNote || null,
455
+ HANDOFF_CONTEXT: {
456
+ task: { key: task?.key, title: task?.title, priority: task?.priority },
457
+ parkNote: task?.parkNote || null,
409
458
  branch,
410
- handoffSteps: branch ? [
411
- 'git fetch origin',
412
- `git checkout ${branch}`,
413
- 'git pull origin ' + branch + ' # get any commits the previous developer pushed',
414
- ] : ['No branch linked yet — create one with create_branch'],
415
- cursorRulesNote: task?.cursorRules?.trim() ? `Cursor rules will be restored to .cursor/rules/${task.key?.toLowerCase()}.mdc` : null,
416
- willDo: 'Clear park note and mark task as active again',
459
+ recentCommits: recentCommits.length ? recentCommits.map(c => `${c.sha?.slice(0,7)} ${c.commit?.message?.split('\n')[0]} — ${c.commit?.author?.name}`) : ['No commits found'],
460
+ recentComments,
417
461
  },
462
+ willAutomate: branch ? [
463
+ `git fetch origin`,
464
+ `git checkout ${branch}`,
465
+ `git pull origin ${branch}`,
466
+ task?.cursorRules?.trim() ? `Restore .cursor/rules/${task.key?.toLowerCase()}.mdc` : null,
467
+ 'Post "picked up" comment on task',
468
+ 'Notify previous developer',
469
+ ].filter(Boolean) : ['No branch linked — create one with create_branch after unparking'],
418
470
  requiresConfirmation: true,
419
- message: 'Read the park note and handoff steps above, then call unpark_task again with confirmed=true to take ownership.',
471
+ message: 'Read the handoff context above. Call unpark_task again with confirmed=true to execute all steps automatically.',
420
472
  })
421
473
  }
422
- const res = await api.patch(`/api/tasks/${taskId}/unpark`, {})
423
- // Restore cursor rules file now that this task is active again
474
+
475
+ // ── Auto-execute git branch switch ────────────────────────────────────
476
+ const cwd = repoPath || process.cwd()
477
+ const repoRoot = findRepoRoot(cwd)
478
+ let gitResult = null
479
+
480
+ if (repoRoot && branch) {
481
+ try {
482
+ runGit('fetch origin', repoRoot)
483
+ runGit(`checkout ${branch}`, repoRoot)
484
+ runGit(`pull origin ${branch}`, repoRoot)
485
+ gitResult = { switched: true, branch }
486
+ } catch (e) {
487
+ gitResult = { switched: false, error: e.message.split('\n')[0], manualSteps: [`git fetch origin`, `git checkout ${branch}`, `git pull origin ${branch}`] }
488
+ }
489
+ }
490
+
491
+ // ── Clear park note + server-side comment & notification ──────────────
492
+ await api.patch(`/api/tasks/${taskId}/unpark`, {})
493
+
494
+ // ── Restore cursor rules file ──────────────────────────────────────────
424
495
  let cursorRulesFile = null
425
496
  if (task?.cursorRules?.trim()) {
426
497
  cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules, repoPath)
427
498
  }
428
- return text({ ...(res?.data || {}), cursorRulesFile })
499
+
500
+ return text({
501
+ unparked: true,
502
+ task: { key: task?.key, title: task?.title },
503
+ git: gitResult || { switched: false, reason: 'No branch linked or repo not found' },
504
+ cursorRules: cursorRulesFile ? { restored: true, path: cursorRulesFile } : { restored: false },
505
+ commentPosted: true,
506
+ previousDevNotified: true,
507
+ parkNote: task?.parkNote || null,
508
+ message: gitResult?.switched
509
+ ? `You are now on branch "${branch}". Cursor rules restored. Start coding from where ${task?.parkNote?.parkedBy ? 'the previous developer' : 'you'} left off.`
510
+ : `Branch switch failed — see git.manualSteps. Cursor rules restored.`,
511
+ })
429
512
  }
430
513
  )
431
514
  }
@@ -927,10 +1010,18 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
927
1010
  if (!taskRes?.success) return errorText('Task not found')
928
1011
  const task = taskRes.data.task
929
1012
 
930
- const suggestedBranch = `feature/${task.key?.toLowerCase()}-${task.title
931
- .toLowerCase()
932
- .replace(/[^a-z0-9]+/g, '-')
933
- .slice(0, 40)}`
1013
+ // Include developer name in branch so it's clear who created it
1014
+ let devSlug = ''
1015
+ try {
1016
+ const meRes = await api.get('/api/auth/me')
1017
+ const devName = meRes?.data?.user?.name || meRes?.data?.name || ''
1018
+ devSlug = devName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 15)
1019
+ } catch { /* non-fatal */ }
1020
+
1021
+ const titleSlug = task.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 35)
1022
+ const suggestedBranch = devSlug
1023
+ ? `${devSlug}/feature/${task.key?.toLowerCase()}-${titleSlug}`
1024
+ : `feature/${task.key?.toLowerCase()}-${titleSlug}`
934
1025
 
935
1026
  const hasReadme = typeof task.readmeMarkdown === 'string' && task.readmeMarkdown.trim().length > 0
936
1027
  const hasCursorRules = typeof task.cursorRules === 'string' && task.cursorRules.trim().length > 0
@@ -1729,7 +1820,18 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
1729
1820
  .replace(/[^a-z0-9]+/g, '-')
1730
1821
  .replace(/^-|-$/g, '')
1731
1822
  .slice(0, 35)
1732
- const branchName = `${prefix}/${task.key.toLowerCase()}-${slug}`
1823
+
1824
+ // Include developer name so it's clear who created the branch
1825
+ let devSlug = ''
1826
+ try {
1827
+ const meRes = await api.get('/api/auth/me')
1828
+ const devName = meRes?.data?.user?.name || meRes?.data?.name || ''
1829
+ devSlug = devName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 15)
1830
+ } catch { /* non-fatal */ }
1831
+
1832
+ const branchName = devSlug
1833
+ ? `${devSlug}/${prefix}/${task.key.toLowerCase()}-${slug}`
1834
+ : `${prefix}/${task.key.toLowerCase()}-${slug}`
1733
1835
 
1734
1836
  // Auto-detect local git state
1735
1837
  let gitState = null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "internaltool-mcp",
3
- "version": "1.6.14",
3
+ "version": "1.6.17",
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",