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.
- package/index.js +164 -62
- 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,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
|
-
|
|
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
|
+
} catch { /* remote branch may not exist */ }
|
|
346
347
|
}
|
|
347
348
|
|
|
348
|
-
if (
|
|
349
|
+
if (uncommitted) {
|
|
349
350
|
return text({
|
|
350
351
|
blocked: true,
|
|
351
|
-
reason:
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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: '
|
|
373
|
+
message: 'Review the preview above, then call park_task again with confirmed=true to execute.',
|
|
371
374
|
})
|
|
372
375
|
}
|
|
373
376
|
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
|
396
|
-
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory).
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
|
471
|
+
message: 'Read the handoff context above. Call unpark_task again with confirmed=true to execute all steps automatically.',
|
|
420
472
|
})
|
|
421
473
|
}
|
|
422
|
-
|
|
423
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
.
|
|
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
|
-
|
|
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