internaltool-mcp 1.6.14 → 1.6.19
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/index.js +214 -67
- 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
|
|
305
|
+
`Pause a task and hand it off safely so another developer can continue without losing any work.
|
|
306
306
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
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,209 @@ 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
|
-
|
|
325
|
-
const cwd = repoPath || process.cwd()
|
|
328
|
+
const cwd = repoPath || process.cwd()
|
|
326
329
|
const repoRoot = findRepoRoot(cwd)
|
|
327
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
+
currentBranch = runGit('branch --show-current', repoRoot).trim()
|
|
338
342
|
if (currentBranch) {
|
|
339
|
-
runGit('fetch origin', repoRoot)
|
|
340
|
-
|
|
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
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (gitBlockers.length > 0) {
|
|
349
|
-
return text({
|
|
350
|
-
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.',
|
|
354
|
-
})
|
|
346
|
+
} catch { /* remote branch may not exist */ }
|
|
355
347
|
}
|
|
356
348
|
|
|
357
349
|
if (!confirmed) {
|
|
358
350
|
return text({
|
|
359
351
|
preview: {
|
|
360
|
-
action: 'park_task',
|
|
361
|
-
taskId,
|
|
362
352
|
task: task ? { key: task.key, title: task.title, branch: task.github?.headBranch || null } : null,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
353
|
+
parkNote: { summary: summary || '(empty)', remaining: remaining || '(empty)', blockers: blockers || '(empty)' },
|
|
354
|
+
willAutomate: [
|
|
355
|
+
uncommitted ? `Auto-commit uncommitted changes with message: "wip(${task?.key?.toLowerCase()}): ${(summary || 'parking task').slice(0, 60)}"` : null,
|
|
356
|
+
unpushedCount > 0 || uncommitted ? `Push branch to remote` : 'Branch is already up to date on remote',
|
|
357
|
+
'Save park note to task',
|
|
358
|
+
task?.cursorRules?.trim() ? `Delete cursor rules file (.cursor/rules/${task.key?.toLowerCase()}.mdc)` : null,
|
|
359
|
+
'Post handoff comment on the task',
|
|
360
|
+
'Notify project members',
|
|
361
|
+
].filter(Boolean),
|
|
368
362
|
},
|
|
369
363
|
requiresConfirmation: true,
|
|
370
|
-
message: '
|
|
364
|
+
message: 'Review the preview above, then call park_task again with confirmed=true to execute.',
|
|
371
365
|
})
|
|
372
366
|
}
|
|
373
367
|
|
|
374
|
-
|
|
375
|
-
|
|
368
|
+
// ── Auto-commit if there are uncommitted changes ───────────────────────
|
|
369
|
+
if (repoRoot && uncommitted) {
|
|
370
|
+
try {
|
|
371
|
+
const commitMsg = `wip(${task?.key?.toLowerCase() || 'task'}): ${(summary || 'parking task').slice(0, 60)}`
|
|
372
|
+
runGit('add -A', repoRoot)
|
|
373
|
+
runGit(`commit -m "${commitMsg}"`, repoRoot)
|
|
374
|
+
unpushedCount += 1 // just committed, so now there's something to push
|
|
375
|
+
} catch (e) {
|
|
376
|
+
return text({ blocked: true, reason: `Auto-commit failed: ${e.message.split('\n')[0]}. Commit manually then call park_task again.` })
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Auto-push unpushed commits ─────────────────────────────────────────
|
|
381
|
+
let pushed = false
|
|
382
|
+
if (repoRoot && currentBranch && unpushedCount > 0) {
|
|
383
|
+
try {
|
|
384
|
+
runGit(`push origin ${currentBranch}`, repoRoot)
|
|
385
|
+
pushed = true
|
|
386
|
+
} catch (e) {
|
|
387
|
+
return text({ blocked: true, reason: `Auto-push failed: ${e.message.split('\n')[0]}. Push manually then call park_task again.` })
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── Save park note + server-side comment & notifications ──────────────
|
|
392
|
+
await api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers })
|
|
393
|
+
|
|
394
|
+
// ── Delete cursor rules file ───────────────────────────────────────────
|
|
376
395
|
let cursorRulesCleared = null
|
|
377
396
|
if (task?.cursorRules?.trim()) {
|
|
378
397
|
cursorRulesCleared = deleteCursorRulesFile(task.key, repoPath)
|
|
379
398
|
}
|
|
399
|
+
|
|
380
400
|
return text({
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
401
|
+
parked: true,
|
|
402
|
+
task: { key: task?.key, title: task?.title, branch: task?.github?.headBranch || null },
|
|
403
|
+
autoPushed: pushed ? `${unpushedCount} commit(s) pushed to origin/${currentBranch}` : 'No push needed',
|
|
404
|
+
cursorRulesCleared: cursorRulesCleared ? `Deleted ${cursorRulesCleared}` : null,
|
|
405
|
+
commentPosted: true,
|
|
406
|
+
teamNotified: true,
|
|
407
|
+
message: 'Task parked. Project members have been notified. Dev B can run unpark_task to continue.',
|
|
386
408
|
})
|
|
387
409
|
}
|
|
388
410
|
)
|
|
389
411
|
|
|
390
412
|
server.tool(
|
|
391
413
|
'unpark_task',
|
|
392
|
-
|
|
414
|
+
`Pick up a parked task and get fully set up to continue the previous developer's work.
|
|
415
|
+
|
|
416
|
+
WHAT THIS DOES AUTOMATICALLY (confirmed=true):
|
|
417
|
+
1. Shows the full park note from the previous developer before doing anything
|
|
418
|
+
2. Shows the last 5 commits on the branch so you know the exact state of the code
|
|
419
|
+
3. Shows recent task comments so you have full context
|
|
420
|
+
4. Runs: git fetch origin + git checkout <branch> + git pull (switches your local repo)
|
|
421
|
+
5. Restores the cursor rules file (.cursor/rules/<task>.mdc) so Cursor follows task rules
|
|
422
|
+
6. Posts a comment that you picked up the task
|
|
423
|
+
7. Notifies the previous developer that you have taken over
|
|
424
|
+
|
|
425
|
+
Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
393
426
|
{
|
|
394
427
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
395
|
-
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the
|
|
396
|
-
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory).
|
|
428
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the park note'),
|
|
429
|
+
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory).'),
|
|
397
430
|
},
|
|
398
431
|
async ({ taskId, confirmed = false, repoPath }) => {
|
|
399
432
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
400
433
|
const task = taskRes?.data?.task
|
|
401
434
|
const branch = task?.github?.headBranch || null
|
|
435
|
+
|
|
436
|
+
// ── Fetch recent commits on the branch ────────────────────────────────
|
|
437
|
+
let recentCommits = []
|
|
438
|
+
if (branch) {
|
|
439
|
+
try {
|
|
440
|
+
const commitsRes = await api.get(`/api/projects/${task.project}/github/commits?per_page=5&branch=${branch}`)
|
|
441
|
+
recentCommits = commitsRes?.data?.commits || []
|
|
442
|
+
} catch { /* non-fatal */ }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Fetch recent task comments ─────────────────────────────────────────
|
|
446
|
+
let recentComments = []
|
|
447
|
+
try {
|
|
448
|
+
const commentsRes = await api.get(`/api/tasks/${taskId}/comments`)
|
|
449
|
+
recentComments = (commentsRes?.data?.comments || []).slice(-5).map(c => ({
|
|
450
|
+
author: c.author?.name || c.author?.email || 'unknown',
|
|
451
|
+
body: c.body?.slice(0, 300),
|
|
452
|
+
at: c.createdAt,
|
|
453
|
+
}))
|
|
454
|
+
} catch { /* non-fatal */ }
|
|
455
|
+
|
|
402
456
|
if (!confirmed) {
|
|
403
457
|
return text({
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
task: task ? { key: task.key, title: task.title, column: task.column } : null,
|
|
408
|
-
parkNote: task?.parkNote || null,
|
|
458
|
+
HANDOFF_CONTEXT: {
|
|
459
|
+
task: { key: task?.key, title: task?.title, priority: task?.priority },
|
|
460
|
+
parkNote: task?.parkNote || null,
|
|
409
461
|
branch,
|
|
410
|
-
|
|
411
|
-
|
|
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',
|
|
462
|
+
recentCommits: recentCommits.length ? recentCommits.map(c => `${c.sha?.slice(0,7)} ${c.commit?.message?.split('\n')[0]} — ${c.commit?.author?.name}`) : ['No commits found'],
|
|
463
|
+
recentComments,
|
|
417
464
|
},
|
|
465
|
+
willAutomate: branch ? [
|
|
466
|
+
`git fetch origin`,
|
|
467
|
+
`git checkout ${branch}`,
|
|
468
|
+
`git pull origin ${branch}`,
|
|
469
|
+
task?.cursorRules?.trim() ? `Restore .cursor/rules/${task.key?.toLowerCase()}.mdc` : null,
|
|
470
|
+
'Post "picked up" comment on task',
|
|
471
|
+
'Notify previous developer',
|
|
472
|
+
].filter(Boolean) : ['No branch linked — create one with create_branch after unparking'],
|
|
418
473
|
requiresConfirmation: true,
|
|
419
|
-
message: 'Read the
|
|
474
|
+
message: 'Read the handoff context above. Call unpark_task again with confirmed=true to execute all steps automatically.',
|
|
420
475
|
})
|
|
421
476
|
}
|
|
422
|
-
|
|
423
|
-
//
|
|
477
|
+
|
|
478
|
+
// ── Auto-execute git branch switch ────────────────────────────────────
|
|
479
|
+
const cwd = repoPath || process.cwd()
|
|
480
|
+
const repoRoot = findRepoRoot(cwd)
|
|
481
|
+
let gitResult = null
|
|
482
|
+
|
|
483
|
+
if (repoRoot && branch) {
|
|
484
|
+
try {
|
|
485
|
+
runGit('fetch origin', repoRoot)
|
|
486
|
+
runGit(`checkout ${branch}`, repoRoot)
|
|
487
|
+
runGit(`pull origin ${branch}`, repoRoot)
|
|
488
|
+
|
|
489
|
+
// Auto-rebase from main so Dev B starts on a clean, up-to-date branch
|
|
490
|
+
let rebaseResult = null
|
|
491
|
+
try {
|
|
492
|
+
const behind = parseInt(runGit(`rev-list HEAD..origin/main --count`, repoRoot).trim(), 10) || 0
|
|
493
|
+
if (behind > 0) {
|
|
494
|
+
runGit('rebase origin/main', repoRoot)
|
|
495
|
+
runGit(`push origin ${branch} --force-with-lease`, repoRoot)
|
|
496
|
+
rebaseResult = { rebased: true, commitsBehind: behind, message: `Auto-rebased ${behind} commit(s) from main — branch is now up to date` }
|
|
497
|
+
} else {
|
|
498
|
+
rebaseResult = { rebased: false, message: 'Branch is already up to date with main' }
|
|
499
|
+
}
|
|
500
|
+
} catch (rebaseErr) {
|
|
501
|
+
rebaseResult = { rebased: false, conflict: true, message: 'Auto-rebase failed — merge conflict detected. Run: git rebase origin/main and resolve conflicts manually.' }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
gitResult = { switched: true, branch, rebase: rebaseResult }
|
|
505
|
+
} catch (e) {
|
|
506
|
+
gitResult = { switched: false, error: e.message.split('\n')[0], manualSteps: [`git fetch origin`, `git checkout ${branch}`, `git pull origin ${branch}`, 'git rebase origin/main'] }
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ── Clear park note + server-side comment & notification ──────────────
|
|
511
|
+
await api.patch(`/api/tasks/${taskId}/unpark`, {})
|
|
512
|
+
|
|
513
|
+
// ── Restore cursor rules file ──────────────────────────────────────────
|
|
424
514
|
let cursorRulesFile = null
|
|
425
515
|
if (task?.cursorRules?.trim()) {
|
|
426
516
|
cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules, repoPath)
|
|
427
517
|
}
|
|
428
|
-
|
|
518
|
+
|
|
519
|
+
return text({
|
|
520
|
+
unparked: true,
|
|
521
|
+
task: { key: task?.key, title: task?.title },
|
|
522
|
+
git: gitResult || { switched: false, reason: 'No branch linked or repo not found' },
|
|
523
|
+
cursorRules: cursorRulesFile ? { restored: true, path: cursorRulesFile } : { restored: false },
|
|
524
|
+
commentPosted: true,
|
|
525
|
+
previousDevNotified: true,
|
|
526
|
+
parkNote: task?.parkNote || null,
|
|
527
|
+
message: gitResult?.switched
|
|
528
|
+
? `You are now on branch "${branch}". Cursor rules restored. Start coding from where ${task?.parkNote?.parkedBy ? 'the previous developer' : 'you'} left off.`
|
|
529
|
+
: `Branch switch failed — see git.manualSteps. Cursor rules restored.`,
|
|
530
|
+
})
|
|
429
531
|
}
|
|
430
532
|
)
|
|
431
533
|
}
|
|
@@ -927,10 +1029,18 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
927
1029
|
if (!taskRes?.success) return errorText('Task not found')
|
|
928
1030
|
const task = taskRes.data.task
|
|
929
1031
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
.
|
|
1032
|
+
// Include developer name in branch so it's clear who created it
|
|
1033
|
+
let devSlug = ''
|
|
1034
|
+
try {
|
|
1035
|
+
const meRes = await api.get('/api/auth/me')
|
|
1036
|
+
const devName = meRes?.data?.user?.name || meRes?.data?.name || ''
|
|
1037
|
+
devSlug = devName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 15)
|
|
1038
|
+
} catch { /* non-fatal */ }
|
|
1039
|
+
|
|
1040
|
+
const titleSlug = task.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 35)
|
|
1041
|
+
const suggestedBranch = devSlug
|
|
1042
|
+
? `${devSlug}/feature/${task.key?.toLowerCase()}-${titleSlug}`
|
|
1043
|
+
: `feature/${task.key?.toLowerCase()}-${titleSlug}`
|
|
934
1044
|
|
|
935
1045
|
const hasReadme = typeof task.readmeMarkdown === 'string' && task.readmeMarkdown.trim().length > 0
|
|
936
1046
|
const hasCursorRules = typeof task.cursorRules === 'string' && task.cursorRules.trim().length > 0
|
|
@@ -1729,7 +1839,18 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
|
|
|
1729
1839
|
.replace(/[^a-z0-9]+/g, '-')
|
|
1730
1840
|
.replace(/^-|-$/g, '')
|
|
1731
1841
|
.slice(0, 35)
|
|
1732
|
-
|
|
1842
|
+
|
|
1843
|
+
// Include developer name so it's clear who created the branch
|
|
1844
|
+
let devSlug = ''
|
|
1845
|
+
try {
|
|
1846
|
+
const meRes = await api.get('/api/auth/me')
|
|
1847
|
+
const devName = meRes?.data?.user?.name || meRes?.data?.name || ''
|
|
1848
|
+
devSlug = devName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 15)
|
|
1849
|
+
} catch { /* non-fatal */ }
|
|
1850
|
+
|
|
1851
|
+
const branchName = devSlug
|
|
1852
|
+
? `${devSlug}/${prefix}/${task.key.toLowerCase()}-${slug}`
|
|
1853
|
+
: `${prefix}/${task.key.toLowerCase()}-${slug}`
|
|
1733
1854
|
|
|
1734
1855
|
// Auto-detect local git state
|
|
1735
1856
|
let gitState = null
|
|
@@ -2351,6 +2472,32 @@ branchAction values (only needed when current branch ≠ task branch):
|
|
|
2351
2472
|
...unsafeUntracked.map(f => `⚠️ SKIP: ${f} ← do not commit this`),
|
|
2352
2473
|
]
|
|
2353
2474
|
|
|
2475
|
+
// ── Block if task is parked — prevent Dev A pushing after handoff ────────
|
|
2476
|
+
if (task) {
|
|
2477
|
+
const isParked = !!(task.parkNote?.parkedAt)
|
|
2478
|
+
const currentOwner = task.parkNote?.currentOwner
|
|
2479
|
+
const parkedBy = task.parkNote?.parkedBy
|
|
2480
|
+
let meId = ''
|
|
2481
|
+
try { const me = await api.get('/api/auth/me'); meId = me?.data?.user?._id || me?.data?._id || '' } catch { /* non-fatal */ }
|
|
2482
|
+
|
|
2483
|
+
if (isParked && parkedBy && parkedBy.toString() === meId) {
|
|
2484
|
+
return text({
|
|
2485
|
+
blocked: true,
|
|
2486
|
+
reason: `You parked "${task.title}". Another developer may have picked it up.`,
|
|
2487
|
+
advice: 'Check the task on the board before pushing. If you still own it, call unpark_task first, then commit.',
|
|
2488
|
+
message: 'Commit blocked — task is parked.',
|
|
2489
|
+
})
|
|
2490
|
+
}
|
|
2491
|
+
if (currentOwner && currentOwner.toString() !== meId && meId) {
|
|
2492
|
+
return text({
|
|
2493
|
+
blocked: true,
|
|
2494
|
+
reason: `"${task.title}" is currently owned by another developer who unparked it.`,
|
|
2495
|
+
advice: 'Do not push to this branch. Coordinate with the current owner first.',
|
|
2496
|
+
message: 'Commit blocked — another developer owns this task.',
|
|
2497
|
+
})
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2354
2501
|
// ── PREVIEW (confirmed=false) ─────────────────────────────────────────────
|
|
2355
2502
|
if (!confirmed) {
|
|
2356
2503
|
// Case 1: branch mismatch — must resolve before anything else
|
package/package.json
CHANGED