internaltool-mcp 1.6.40 → 1.6.41
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 +231 -44
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
19
19
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
20
20
|
import { execSync } from 'child_process'
|
|
21
|
-
import { mkdirSync, writeFileSync, unlinkSync, existsSync, readdirSync, statSync } from 'fs'
|
|
21
|
+
import { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync, readdirSync, statSync } from 'fs'
|
|
22
22
|
import { join } from 'path'
|
|
23
23
|
import { z } from 'zod'
|
|
24
24
|
import { api, login, configure } from './api-client.js'
|
|
@@ -83,6 +83,180 @@ async function assertAdmin() {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// ── Codebase analysis helpers (used by plan_task_from_codebase) ───────────────
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reads manifest files in cwd and returns detected stack info.
|
|
90
|
+
* Works for any language — JS/TS, Python, Go, Ruby, Java, Rust, PHP.
|
|
91
|
+
*/
|
|
92
|
+
function detectStack(cwd) {
|
|
93
|
+
const stack = { language: null, framework: null, testRunner: null, packageManager: null, extra: [] }
|
|
94
|
+
try {
|
|
95
|
+
// Node.js / JS / TS
|
|
96
|
+
const pkgPath = join(cwd, 'package.json')
|
|
97
|
+
if (existsSync(pkgPath)) {
|
|
98
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
99
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
100
|
+
stack.language = pkg.scripts?.build?.includes('tsc') || existsSync(join(cwd, 'tsconfig.json')) ? 'typescript' : 'javascript'
|
|
101
|
+
stack.packageManager = existsSync(join(cwd, 'pnpm-lock.yaml')) ? 'pnpm' : existsSync(join(cwd, 'yarn.lock')) ? 'yarn' : 'npm'
|
|
102
|
+
// Framework detection
|
|
103
|
+
if (deps.next) stack.framework = 'nextjs'
|
|
104
|
+
else if (deps.nuxt) stack.framework = 'nuxt'
|
|
105
|
+
else if (deps.react) stack.framework = 'react'
|
|
106
|
+
else if (deps.vue) stack.framework = 'vue'
|
|
107
|
+
else if (deps.svelte) stack.framework = 'svelte'
|
|
108
|
+
else if (deps.express) stack.framework = 'express'
|
|
109
|
+
else if (deps.fastify) stack.framework = 'fastify'
|
|
110
|
+
else if (deps.nestjs || deps['@nestjs/core']) stack.framework = 'nestjs'
|
|
111
|
+
else if (deps.hono) stack.framework = 'hono'
|
|
112
|
+
// Test runner
|
|
113
|
+
if (deps.jest || deps['@jest/core']) stack.testRunner = 'jest'
|
|
114
|
+
else if (deps.vitest) stack.testRunner = 'vitest'
|
|
115
|
+
else if (deps.mocha) stack.testRunner = 'mocha'
|
|
116
|
+
else if (deps.playwright || deps['@playwright/test']) stack.testRunner = 'playwright'
|
|
117
|
+
// ORM / DB
|
|
118
|
+
if (deps.mongoose) stack.extra.push('mongoose')
|
|
119
|
+
if (deps.prisma) stack.extra.push('prisma')
|
|
120
|
+
if (deps.drizzle) stack.extra.push('drizzle')
|
|
121
|
+
if (deps.typeorm) stack.extra.push('typeorm')
|
|
122
|
+
if (deps.sequelize) stack.extra.push('sequelize')
|
|
123
|
+
if (deps['@supabase/supabase-js']) stack.extra.push('supabase')
|
|
124
|
+
if (deps.trpc || deps['@trpc/server']) stack.extra.push('trpc')
|
|
125
|
+
return stack
|
|
126
|
+
}
|
|
127
|
+
// Python
|
|
128
|
+
const reqPath = join(cwd, 'requirements.txt')
|
|
129
|
+
const pyprojPath = join(cwd, 'pyproject.toml')
|
|
130
|
+
if (existsSync(reqPath) || existsSync(pyprojPath)) {
|
|
131
|
+
stack.language = 'python'
|
|
132
|
+
const content = existsSync(reqPath) ? readFileSync(reqPath, 'utf8') : readFileSync(pyprojPath, 'utf8')
|
|
133
|
+
if (content.includes('django')) stack.framework = 'django'
|
|
134
|
+
else if (content.includes('fastapi')) stack.framework = 'fastapi'
|
|
135
|
+
else if (content.includes('flask')) stack.framework = 'flask'
|
|
136
|
+
else if (content.includes('starlette')) stack.framework = 'starlette'
|
|
137
|
+
if (content.includes('pytest')) stack.testRunner = 'pytest'
|
|
138
|
+
if (content.includes('sqlalchemy')) stack.extra.push('sqlalchemy')
|
|
139
|
+
if (content.includes('alembic')) stack.extra.push('alembic')
|
|
140
|
+
return stack
|
|
141
|
+
}
|
|
142
|
+
// Go
|
|
143
|
+
if (existsSync(join(cwd, 'go.mod'))) {
|
|
144
|
+
stack.language = 'go'
|
|
145
|
+
const gomod = readFileSync(join(cwd, 'go.mod'), 'utf8')
|
|
146
|
+
if (gomod.includes('gin-gonic/gin')) stack.framework = 'gin'
|
|
147
|
+
else if (gomod.includes('go-chi/chi')) stack.framework = 'chi'
|
|
148
|
+
else if (gomod.includes('labstack/echo')) stack.framework = 'echo'
|
|
149
|
+
else if (gomod.includes('gofiber/fiber')) stack.framework = 'fiber'
|
|
150
|
+
stack.testRunner = 'go test'
|
|
151
|
+
return stack
|
|
152
|
+
}
|
|
153
|
+
// Ruby
|
|
154
|
+
if (existsSync(join(cwd, 'Gemfile'))) {
|
|
155
|
+
stack.language = 'ruby'
|
|
156
|
+
const gemfile = readFileSync(join(cwd, 'Gemfile'), 'utf8')
|
|
157
|
+
if (gemfile.includes('rails')) stack.framework = 'rails'
|
|
158
|
+
else if (gemfile.includes('sinatra')) stack.framework = 'sinatra'
|
|
159
|
+
if (gemfile.includes('rspec')) stack.testRunner = 'rspec'
|
|
160
|
+
return stack
|
|
161
|
+
}
|
|
162
|
+
// Rust
|
|
163
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) {
|
|
164
|
+
stack.language = 'rust'
|
|
165
|
+
const cargo = readFileSync(join(cwd, 'Cargo.toml'), 'utf8')
|
|
166
|
+
if (cargo.includes('actix-web')) stack.framework = 'actix-web'
|
|
167
|
+
else if (cargo.includes('axum')) stack.framework = 'axum'
|
|
168
|
+
else if (cargo.includes('warp')) stack.framework = 'warp'
|
|
169
|
+
stack.testRunner = 'cargo test'
|
|
170
|
+
return stack
|
|
171
|
+
}
|
|
172
|
+
// Java / Kotlin
|
|
173
|
+
if (existsSync(join(cwd, 'pom.xml')) || existsSync(join(cwd, 'build.gradle'))) {
|
|
174
|
+
stack.language = existsSync(join(cwd, 'build.gradle.kts')) ? 'kotlin' : 'java'
|
|
175
|
+
stack.framework = 'spring-boot'
|
|
176
|
+
stack.testRunner = 'junit'
|
|
177
|
+
return stack
|
|
178
|
+
}
|
|
179
|
+
// PHP
|
|
180
|
+
if (existsSync(join(cwd, 'composer.json'))) {
|
|
181
|
+
stack.language = 'php'
|
|
182
|
+
const composer = JSON.parse(readFileSync(join(cwd, 'composer.json'), 'utf8'))
|
|
183
|
+
const req = { ...composer.require, ...composer['require-dev'] }
|
|
184
|
+
if (req['laravel/framework']) stack.framework = 'laravel'
|
|
185
|
+
else if (req['symfony/symfony'] || req['symfony/framework-bundle']) stack.framework = 'symfony'
|
|
186
|
+
return stack
|
|
187
|
+
}
|
|
188
|
+
} catch { /* non-fatal */ }
|
|
189
|
+
return stack
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns a shallow directory tree (depth 2) so the agent knows the folder layout.
|
|
194
|
+
* Ignores node_modules, .git, vendor, __pycache__, dist, build, .next
|
|
195
|
+
*/
|
|
196
|
+
function getDirTree(cwd, maxDepth = 2) {
|
|
197
|
+
const IGNORE = new Set(['node_modules', '.git', 'vendor', '__pycache__', 'dist', 'build', '.next', '.turbo', 'coverage', '.cache', 'target', 'venv', '.venv'])
|
|
198
|
+
function walk(dir, depth) {
|
|
199
|
+
if (depth > maxDepth) return []
|
|
200
|
+
let entries
|
|
201
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return [] }
|
|
202
|
+
return entries
|
|
203
|
+
.filter(e => !IGNORE.has(e.name) && !e.name.startsWith('.'))
|
|
204
|
+
.map(e => {
|
|
205
|
+
if (e.isDirectory()) {
|
|
206
|
+
const children = walk(join(dir, e.name), depth + 1)
|
|
207
|
+
return { name: e.name + '/', children: children.length ? children : undefined }
|
|
208
|
+
}
|
|
209
|
+
return { name: e.name }
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
return walk(cwd, 0)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Based on detected stack, grep for the most likely entry-point files.
|
|
217
|
+
* Returns a list of { pattern, matches } — actual file paths found locally.
|
|
218
|
+
*/
|
|
219
|
+
function findEntryPoints(cwd, stack) {
|
|
220
|
+
const results = {}
|
|
221
|
+
const greps = {
|
|
222
|
+
javascript: { typescript: true,
|
|
223
|
+
routes: ['router\\.', 'app\\.get\\|app\\.post\\|app\\.put\\|fastify\\.', 'Route path='],
|
|
224
|
+
models: ['mongoose\\.Schema\\|new Schema\\|@Entity\\|@Table'],
|
|
225
|
+
components: ['export default function\\|export const.*=.*=>\\|React\\.FC'],
|
|
226
|
+
},
|
|
227
|
+
python: {
|
|
228
|
+
routes: ['@app\\.route\\|@router\\.\\|path(\\|urlpatterns\\|@api_view'],
|
|
229
|
+
models: ['class.*Model\\|db\\.Model\\|Base\\)\\|models\\.Model'],
|
|
230
|
+
schemas: ['class.*Schema\\|serializers\\.\\|Pydantic\\|BaseModel'],
|
|
231
|
+
},
|
|
232
|
+
go: {
|
|
233
|
+
routes: ['router\\.\\.\\|http\\.HandleFunc\\|r\\.GET\\|r\\.POST\\|e\\.GET'],
|
|
234
|
+
models: ['type.*struct\\|gorm\\.Model'],
|
|
235
|
+
},
|
|
236
|
+
ruby: {
|
|
237
|
+
routes: ['resources \\|get \'\\|post \'\\|Rails\\.application\\.routes'],
|
|
238
|
+
models: ['ApplicationRecord\\|ActiveRecord::Base\\|belongs_to\\|has_many'],
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const lang = stack.language === 'typescript' ? 'javascript' : stack.language
|
|
243
|
+
const patterns = greps[lang] || {}
|
|
244
|
+
|
|
245
|
+
for (const [type, patternList] of Object.entries(patterns)) {
|
|
246
|
+
if (type === 'typescript') continue
|
|
247
|
+
try {
|
|
248
|
+
const pat = Array.isArray(patternList) ? patternList.join('\\|') : patternList
|
|
249
|
+
const out = execSync(
|
|
250
|
+
`grep -rl "${pat}" . --include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" --include="*.py" --include="*.go" --include="*.rb" --include="*.java" --include="*.kt" --include="*.rs" --include="*.php" 2>/dev/null | grep -v node_modules | grep -v ".git" | head -8`,
|
|
251
|
+
{ cwd, encoding: 'utf8', timeout: 4000 }
|
|
252
|
+
)
|
|
253
|
+
const files = out.trim().split('\n').filter(Boolean)
|
|
254
|
+
if (files.length) results[type] = files
|
|
255
|
+
} catch { /* non-fatal */ }
|
|
256
|
+
}
|
|
257
|
+
return results
|
|
258
|
+
}
|
|
259
|
+
|
|
86
260
|
// ── Tool registration functions ───────────────────────────────────────────────
|
|
87
261
|
|
|
88
262
|
function registerAuthTools(server) {
|
|
@@ -370,7 +544,13 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
370
544
|
return errorText(`Access denied: session is scoped to project ${scopedProjectId}`)
|
|
371
545
|
}
|
|
372
546
|
|
|
373
|
-
//
|
|
547
|
+
// ── 1. Run codebase analysis using local filesystem access ────────────────
|
|
548
|
+
const cwd = process.cwd()
|
|
549
|
+
const stack = detectStack(cwd)
|
|
550
|
+
const dirTree = getDirTree(cwd, 2)
|
|
551
|
+
const entryPoints = findEntryPoints(cwd, stack)
|
|
552
|
+
|
|
553
|
+
// ── 2. Fetch project context ──────────────────────────────────────────────
|
|
374
554
|
let projectContext = null
|
|
375
555
|
try {
|
|
376
556
|
const projRes = await api.get(`/api/projects/${projectId}`)
|
|
@@ -384,63 +564,58 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
384
564
|
}
|
|
385
565
|
} catch { /* non-fatal */ }
|
|
386
566
|
|
|
387
|
-
//
|
|
388
|
-
let
|
|
567
|
+
// ── 3. Duplicate check ────────────────────────────────────────────────────
|
|
568
|
+
let similarTasks = null
|
|
389
569
|
try {
|
|
390
570
|
const keywords = request.split(' ').filter(w => w.length > 4).slice(0, 3).join(' ')
|
|
391
571
|
const searchRes = await api.get(
|
|
392
572
|
`/api/projects/${projectId}/tasks/search?q=${encodeURIComponent(keywords)}&limit=3`
|
|
393
573
|
)
|
|
394
574
|
if (searchRes?.success && searchRes.data?.tasks?.length > 0) {
|
|
395
|
-
|
|
396
|
-
taskId:
|
|
397
|
-
key: t.key,
|
|
398
|
-
title: t.title,
|
|
399
|
-
column: t.column,
|
|
400
|
-
taskType: t.taskType,
|
|
575
|
+
similarTasks = searchRes.data.tasks.map(t => ({
|
|
576
|
+
taskId: t._id, key: t.key, title: t.title, column: t.column, taskType: t.taskType,
|
|
401
577
|
}))
|
|
402
578
|
}
|
|
403
579
|
} catch { /* non-fatal */ }
|
|
404
580
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
instruction: [
|
|
411
|
-
'If one of these IS the same task: call kickoff_task on it instead of creating a new one.',
|
|
412
|
-
'If none match: proceed with the analysis protocol below.',
|
|
413
|
-
],
|
|
414
|
-
analysisProtocol: {
|
|
415
|
-
step1: 'Read package.json / go.mod / requirements.txt to identify the stack',
|
|
416
|
-
step2: 'Grep for existing patterns relevant to the request',
|
|
417
|
-
step3: 'Identify files to create and modify',
|
|
418
|
-
step4: `Call create_task(projectId="${projectId}", title="...", readmeMarkdown="...", taskType="...", priority="${priority}", column="todo", subtasks=[...], suggestedFiles=[...])`,
|
|
419
|
-
step5: 'Call kickoff_task(taskId=<id>, confirmed=true, agentRole="builder", files=[...suggestedFiles])',
|
|
420
|
-
},
|
|
421
|
-
projectContext,
|
|
422
|
-
request,
|
|
423
|
-
projectId,
|
|
424
|
-
priority,
|
|
425
|
-
})
|
|
426
|
-
}
|
|
581
|
+
// ── 4. Return codebase intelligence + tight instructions ──────────────────
|
|
582
|
+
const hasEntryPoints = Object.keys(entryPoints).length > 0
|
|
583
|
+
const stackSummary = [
|
|
584
|
+
stack.language, stack.framework, stack.testRunner, ...(stack.extra || [])
|
|
585
|
+
].filter(Boolean).join(', ') || 'unknown (no manifest found at cwd)'
|
|
427
586
|
|
|
428
|
-
// No duplicate — return the analysis brief
|
|
429
587
|
return text({
|
|
430
|
-
|
|
588
|
+
// Real codebase data — use these directly, do NOT re-read what's already here
|
|
589
|
+
codebaseIntelligence: {
|
|
590
|
+
cwd,
|
|
591
|
+
stack: { ...stack, summary: stackSummary },
|
|
592
|
+
dirTree,
|
|
593
|
+
entryPoints: hasEntryPoints
|
|
594
|
+
? entryPoints
|
|
595
|
+
: { note: 'No entry points found at cwd — check that MCP server is running from the project root' },
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
// Duplicate guard
|
|
599
|
+
...(similarTasks?.length > 0 && {
|
|
600
|
+
duplicateWarning: true,
|
|
601
|
+
similarTasks,
|
|
602
|
+
duplicateInstruction: 'Check the list above. If one matches → call kickoff_task on it. If none match → proceed.',
|
|
603
|
+
}),
|
|
604
|
+
|
|
605
|
+
// What to do now — use codebaseIntelligence above to skip re-reading the filesystem
|
|
606
|
+
nextSteps: [
|
|
607
|
+
'Use dirTree + entryPoints above to identify which files need changing — you already have the map.',
|
|
608
|
+
`Read 2-3 files from entryPoints.routes / entryPoints.models / entryPoints.components to extract naming conventions and wiring patterns.`,
|
|
609
|
+
'Draft readmeMarkdown with: ## Goal, ## Stack, ## Technical approach (name real files), ## Files to create, ## Files to modify, ## Subtasks (ordered), ## Acceptance criteria.',
|
|
610
|
+
`Call create_task(projectId="${projectId}", title="<verb + noun>", readmeMarkdown="<plan>", taskType="<feature|bugfix|...>", priority="${priority}", column="todo", subtasks=[...], suggestedFiles=[...])`,
|
|
611
|
+
'Immediately after: call kickoff_task(taskId=<returned id>, confirmed=true, agentRole="builder", files=[...suggestedFiles])',
|
|
612
|
+
],
|
|
613
|
+
|
|
614
|
+
// Context
|
|
431
615
|
request,
|
|
616
|
+
projectId,
|
|
432
617
|
priority,
|
|
433
618
|
projectContext,
|
|
434
|
-
analysisProtocol: {
|
|
435
|
-
overview: 'Follow these steps IN ORDER before calling create_task',
|
|
436
|
-
step1_stack: 'Read package.json / go.mod / requirements.txt / Cargo.toml — detect language, framework, test runner',
|
|
437
|
-
step2_entrypoint: 'Grep for the relevant area: routes, controllers, components, models — find where similar features live',
|
|
438
|
-
step3_patterns: 'Read 2-3 existing similar files — note naming conventions, folder structure, wiring patterns',
|
|
439
|
-
step4_impact: 'List every file to CREATE and every file to MODIFY — be exhaustive',
|
|
440
|
-
step5_create: `Call create_task(projectId="${projectId}", title="<action-verb noun>", readmeMarkdown="<full plan>", taskType="<type>", priority="${priority}", column="todo", subtasks=[{title: "step1"}, ...], suggestedFiles=["path/to/file1", ...])`,
|
|
441
|
-
step6_kickoff: 'Call kickoff_task(taskId=<returned id>, confirmed=true, agentRole="builder", files=[...suggestedFiles])',
|
|
442
|
-
},
|
|
443
|
-
reminder: 'Do NOT call create_task before completing steps 1-4. The readmeMarkdown must reference real file paths from your analysis.',
|
|
444
619
|
})
|
|
445
620
|
}
|
|
446
621
|
)
|
|
@@ -680,6 +855,9 @@ Set confirmed=false first to preview, then confirmed=true to execute everything.
|
|
|
680
855
|
agentWorkspace: { clearedAt: new Date().toISOString() }
|
|
681
856
|
}).catch(() => {/* non-fatal */})
|
|
682
857
|
|
|
858
|
+
// Remove .internaltool-active-task so the Claude Code hook blocks edits until next kickoff
|
|
859
|
+
try { unlinkSync(join(process.cwd(), '.internaltool-active-task')) } catch { /* non-fatal */ }
|
|
860
|
+
|
|
683
861
|
// ── #9 Capture last commit for handoff metadata ───────────────────────
|
|
684
862
|
const lastCommit = getLastCommitMeta(repoRoot)
|
|
685
863
|
|
|
@@ -3913,6 +4091,15 @@ After \`request_human_input\`: STOP, show the question in chat, wait for reply,
|
|
|
3913
4091
|
}
|
|
3914
4092
|
api.patch(`/api/tasks/${taskId}`, workspacePatch).catch(() => {/* non-fatal */})
|
|
3915
4093
|
|
|
4094
|
+
// Write .internaltool-active-task so the Claude Code hook knows a task is active
|
|
4095
|
+
try {
|
|
4096
|
+
writeFileSync(
|
|
4097
|
+
join(process.cwd(), '.internaltool-active-task'),
|
|
4098
|
+
JSON.stringify({ taskId, taskKey: task.key, title: task.title, agentRole: agentRole || null, kickedOffAt: new Date().toISOString() }, null, 2),
|
|
4099
|
+
'utf8'
|
|
4100
|
+
)
|
|
4101
|
+
} catch { /* non-fatal — hook gracefully degrades if file can't be written */ }
|
|
4102
|
+
|
|
3916
4103
|
// Auto-scan workspace so the Workspace tab in the UI is fresh immediately after kickoff.
|
|
3917
4104
|
if (kickoffProject) {
|
|
3918
4105
|
runWorkspaceScan(taskId, task, kickoffProject, repoPath).catch(() => {/* non-fatal */})
|
package/package.json
CHANGED