@agilemonster/mcp-server 1.0.14 → 1.0.15
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/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/server.js +249 -144
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AgileMonsterProjectConfig, AutonomousStepResponse, Board, BoardAgentConfig, Card, ContextResponse, DiscoveredProject, PulseResponse } from './types.js';
|
|
2
|
-
export declare const MCP_SERVER_VERSION = "1.0.
|
|
2
|
+
export declare const MCP_SERVER_VERSION = "1.0.15";
|
|
3
3
|
export declare class AgileMonsterClient {
|
|
4
4
|
private apiKey;
|
|
5
5
|
private baseUrl;
|
package/dist/client.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -988,15 +988,29 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
988
988
|
await client.addProgress(card.id, provider, `Card is now **In Progress**, but no matching source files were found in \`${projectPath}\`.`);
|
|
989
989
|
return false;
|
|
990
990
|
}
|
|
991
|
-
// 3. Ask Agile Monster Autonomous Step to pick the 1-
|
|
991
|
+
// 3. Ask Agile Monster Autonomous Step to pick the 1-6 most relevant files
|
|
992
992
|
const selectionRes = await client.autonomousStep({
|
|
993
993
|
card_id: card.id,
|
|
994
994
|
action: 'select_files',
|
|
995
995
|
file_index: fileIndex,
|
|
996
996
|
});
|
|
997
|
-
const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0,
|
|
997
|
+
const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0, 6);
|
|
998
998
|
if (selectedFiles.length === 0) {
|
|
999
|
-
selectedFiles.push(...fileIndex.slice(0,
|
|
999
|
+
selectedFiles.push(...fileIndex.slice(0, 4));
|
|
1000
|
+
}
|
|
1001
|
+
// Automatically include project type definition files (e.g. types.ts,frontend/src/lib/types.ts) so the model knows exact schemas
|
|
1002
|
+
for (const candidateTypeFile of [
|
|
1003
|
+
'frontend/src/lib/types.ts',
|
|
1004
|
+
'src/lib/types.ts',
|
|
1005
|
+
'src/types.ts',
|
|
1006
|
+
'src/types/index.ts',
|
|
1007
|
+
'frontend/src/types.ts',
|
|
1008
|
+
]) {
|
|
1009
|
+
if (fileIndex.includes(candidateTypeFile) &&
|
|
1010
|
+
!selectedFiles.includes(candidateTypeFile) &&
|
|
1011
|
+
selectedFiles.length < 7) {
|
|
1012
|
+
selectedFiles.push(candidateTypeFile);
|
|
1013
|
+
}
|
|
1000
1014
|
}
|
|
1001
1015
|
const filesPayload = {};
|
|
1002
1016
|
for (const rel of selectedFiles) {
|
|
@@ -1015,93 +1029,135 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1015
1029
|
.map((f) => `\`${f}\``)
|
|
1016
1030
|
.join(', ')}. Generating code changes...`);
|
|
1017
1031
|
// 4. Generate surgical code edits
|
|
1018
|
-
|
|
1032
|
+
let editsRes = await client.autonomousStep({
|
|
1019
1033
|
card_id: card.id,
|
|
1020
1034
|
action: 'generate_edits',
|
|
1021
1035
|
files: filesPayload,
|
|
1022
1036
|
});
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (searchBraceDelta !== replaceBraceDelta || searchParenDelta !== replaceParenDelta) {
|
|
1049
|
-
continue;
|
|
1050
|
-
}
|
|
1051
|
-
fs.writeFileSync(fullPath, updatedContent, 'utf8');
|
|
1052
|
-
// Pre-submit static analysis gate for Dart/Flutter and PHP files
|
|
1053
|
-
if (relPath.endsWith('.dart')) {
|
|
1054
|
-
try {
|
|
1055
|
-
execSync(`dart analyze "${fullPath}"`, {
|
|
1056
|
-
cwd: projectPath,
|
|
1057
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1058
|
-
timeout: 25000,
|
|
1059
|
-
});
|
|
1037
|
+
const applyEditsPass = (editsList) => {
|
|
1038
|
+
const modFiles = [];
|
|
1039
|
+
for (const edit of editsList) {
|
|
1040
|
+
const relPath = edit.file || edit.path;
|
|
1041
|
+
if (!relPath || !edit.search || typeof edit.replace !== 'string')
|
|
1042
|
+
continue;
|
|
1043
|
+
const fullPath = path.resolve(projectPath, relPath);
|
|
1044
|
+
if (!fullPath.startsWith(path.resolve(projectPath)))
|
|
1045
|
+
continue;
|
|
1046
|
+
if (!fs.existsSync(fullPath))
|
|
1047
|
+
continue;
|
|
1048
|
+
const currentContent = fs.readFileSync(fullPath, 'utf8');
|
|
1049
|
+
if (!originalContents.has(fullPath)) {
|
|
1050
|
+
originalContents.set(fullPath, currentContent);
|
|
1051
|
+
}
|
|
1052
|
+
if (currentContent.includes(edit.search)) {
|
|
1053
|
+
const updatedContent = currentContent.replace(edit.search, edit.replace);
|
|
1054
|
+
if (updatedContent !== currentContent) {
|
|
1055
|
+
const countChar = (s, ch) => s.split(ch).length - 1;
|
|
1056
|
+
const searchBraceDelta = countChar(edit.search, '{') - countChar(edit.search, '}');
|
|
1057
|
+
const replaceBraceDelta = countChar(edit.replace, '{') - countChar(edit.replace, '}');
|
|
1058
|
+
const searchParenDelta = countChar(edit.search, '(') - countChar(edit.search, ')');
|
|
1059
|
+
const replaceParenDelta = countChar(edit.replace, '(') - countChar(edit.replace, ')');
|
|
1060
|
+
if (searchBraceDelta !== replaceBraceDelta || searchParenDelta !== replaceParenDelta) {
|
|
1061
|
+
continue;
|
|
1060
1062
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1063
|
+
fs.writeFileSync(fullPath, updatedContent, 'utf8');
|
|
1064
|
+
if (relPath.endsWith('.dart')) {
|
|
1065
|
+
try {
|
|
1066
|
+
execSync(`dart analyze "${fullPath}"`, {
|
|
1067
|
+
cwd: projectPath,
|
|
1068
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1069
|
+
timeout: 25000,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
catch (analyzeErr) {
|
|
1073
|
+
const out = String(analyzeErr?.stdout || '') + String(analyzeErr?.stderr || '');
|
|
1074
|
+
if (/\berror\s+[•-]/i.test(out)) {
|
|
1075
|
+
fs.writeFileSync(fullPath, currentContent, 'utf8');
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
else if (relPath.endsWith('.php')) {
|
|
1081
|
+
try {
|
|
1082
|
+
execSync(`php -l "${fullPath}"`, {
|
|
1083
|
+
cwd: projectPath,
|
|
1084
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1085
|
+
timeout: 10000,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1065
1089
|
fs.writeFileSync(fullPath, currentContent, 'utf8');
|
|
1066
1090
|
continue;
|
|
1067
1091
|
}
|
|
1068
1092
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
try {
|
|
1072
|
-
execSync(`php -l "${fullPath}"`, {
|
|
1073
|
-
cwd: projectPath,
|
|
1074
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1075
|
-
timeout: 10000,
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
catch {
|
|
1079
|
-
fs.writeFileSync(fullPath, currentContent, 'utf8');
|
|
1080
|
-
continue;
|
|
1093
|
+
if (!modFiles.includes(relPath)) {
|
|
1094
|
+
modFiles.push(relPath);
|
|
1081
1095
|
}
|
|
1082
1096
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return modFiles;
|
|
1100
|
+
};
|
|
1101
|
+
const originalContents = new Map();
|
|
1102
|
+
const modifiedFiles = applyEditsPass(editsRes.edits || []);
|
|
1103
|
+
// Pre-submit frontend build verification gate for root AND monorepo subdirectories (frontend/, web/, client/)
|
|
1104
|
+
const verifyFrontendBuilds = () => {
|
|
1105
|
+
const hasFrontendEdits = modifiedFiles.some((f) => /\.(tsx?|jsx?|vue|css)$/i.test(f));
|
|
1106
|
+
if (!hasFrontendEdits)
|
|
1107
|
+
return { ok: true };
|
|
1108
|
+
const candidateDirs = [
|
|
1109
|
+
projectPath,
|
|
1110
|
+
path.join(projectPath, 'frontend'),
|
|
1111
|
+
path.join(projectPath, 'web'),
|
|
1112
|
+
path.join(projectPath, 'client'),
|
|
1113
|
+
];
|
|
1114
|
+
for (const dir of candidateDirs) {
|
|
1115
|
+
const pkgJsonPath = path.join(dir, 'package.json');
|
|
1116
|
+
if (!fs.existsSync(pkgJsonPath) || !fs.existsSync(path.join(dir, 'node_modules')))
|
|
1117
|
+
continue;
|
|
1118
|
+
try {
|
|
1119
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
1120
|
+
if (pkg?.scripts?.build) {
|
|
1121
|
+
const buildCmd = fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))
|
|
1122
|
+
? 'pnpm run build'
|
|
1123
|
+
: 'npm run build';
|
|
1124
|
+
execSync(buildCmd, {
|
|
1125
|
+
cwd: dir,
|
|
1126
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1127
|
+
timeout: 60000,
|
|
1128
|
+
});
|
|
1085
1129
|
}
|
|
1086
1130
|
}
|
|
1131
|
+
catch (buildErr) {
|
|
1132
|
+
const errOut = (String(buildErr?.stdout || '') + '\n' + String(buildErr?.stderr || '')).trim();
|
|
1133
|
+
return { ok: false, errorOutput: errOut.slice(-1500) };
|
|
1134
|
+
}
|
|
1087
1135
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
execSync('npm run build', {
|
|
1097
|
-
cwd: projectPath,
|
|
1098
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1099
|
-
timeout: 45000,
|
|
1100
|
-
});
|
|
1136
|
+
return { ok: true };
|
|
1137
|
+
};
|
|
1138
|
+
let buildCheck = verifyFrontendBuilds();
|
|
1139
|
+
if (!buildCheck.ok) {
|
|
1140
|
+
// Revert first pass, attach compiler error context, and do 1 self-correction pass
|
|
1141
|
+
for (const [fPath, orig] of originalContents.entries()) {
|
|
1142
|
+
try {
|
|
1143
|
+
fs.writeFileSync(fPath, orig, 'utf8');
|
|
1101
1144
|
}
|
|
1145
|
+
catch { }
|
|
1102
1146
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1147
|
+
modifiedFiles.length = 0;
|
|
1148
|
+
const retryFiles = {
|
|
1149
|
+
...filesPayload,
|
|
1150
|
+
'__COMPILER_ERROR_TO_FIX__.txt': `The previous edit failed to compile with:\n${buildCheck.errorOutput}\nPlease fix the TypeScript/build error using the exact property names from types.ts.`,
|
|
1151
|
+
};
|
|
1152
|
+
editsRes = await client.autonomousStep({
|
|
1153
|
+
card_id: card.id,
|
|
1154
|
+
action: 'generate_edits',
|
|
1155
|
+
files: retryFiles,
|
|
1156
|
+
});
|
|
1157
|
+
const retryMods = applyEditsPass(editsRes.edits || []);
|
|
1158
|
+
modifiedFiles.push(...retryMods);
|
|
1159
|
+
buildCheck = verifyFrontendBuilds();
|
|
1160
|
+
if (!buildCheck.ok) {
|
|
1105
1161
|
for (const [fPath, orig] of originalContents.entries()) {
|
|
1106
1162
|
try {
|
|
1107
1163
|
fs.writeFileSync(fPath, orig, 'utf8');
|
|
@@ -1122,19 +1178,21 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1122
1178
|
}).trim();
|
|
1123
1179
|
}
|
|
1124
1180
|
catch { }
|
|
1181
|
+
// Automatically commit, push, and run the project's defined deployment pipeline (e.g. deploy/deploy.sh, GitHub Actions, or Docker)
|
|
1182
|
+
// so changes are live for testing as soon as the card moves to Testing / QA!
|
|
1183
|
+
const deployInfo = runProjectDeploymentPipeline(projectPath, card.title || 'update', card.id.slice(0, 8));
|
|
1125
1184
|
const baseSummary = editsRes.summary ||
|
|
1126
1185
|
`Updated ${modifiedFiles.join(', ')} to complete "${card.title}".`;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
.replace(/```[\s\S]*?```/g, '')
|
|
1130
|
-
.trim();
|
|
1186
|
+
const cleanPlainSummary = baseSummary.replace(/```[\s\S]*?```/g, '').trim() +
|
|
1187
|
+
(deployInfo.deployedNote ? `\n\n${deployInfo.deployedNote}` : '');
|
|
1131
1188
|
const verificationSteps = editsRes.verification_steps ||
|
|
1132
|
-
`1. Check the
|
|
1189
|
+
`1. Check the live update for "${card.title}".\n2. Click **Approve & Deploy to Done** when verified.`;
|
|
1133
1190
|
const logLines = [
|
|
1134
1191
|
`Claimed card "${card.title}" (${card.id}) in ${projectPath}`,
|
|
1135
1192
|
`Agent: ${workerLabel} | Model: ${card.agent_model || 'auto'}`,
|
|
1136
1193
|
`Selected files: ${Object.keys(filesPayload).join(', ')}`,
|
|
1137
1194
|
`Modified & verified files: ${modifiedFiles.join(', ')}`,
|
|
1195
|
+
...(deployInfo.deployedNote ? [`Deployment: ${deployInfo.deployedNote}`] : []),
|
|
1138
1196
|
`Summary: ${cleanPlainSummary}`,
|
|
1139
1197
|
];
|
|
1140
1198
|
appendIdeActivityLog(projectPath, {
|
|
@@ -1192,46 +1250,28 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1192
1250
|
activeAutonomousCards.delete(card.id);
|
|
1193
1251
|
}
|
|
1194
1252
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
catch {
|
|
1209
|
-
return false;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
const provider = card.agent_provider || 'cursor';
|
|
1213
|
-
const cleanTitle = (card.title || 'update').replace(/["`\r\n]+/g, ' ').trim();
|
|
1253
|
+
/**
|
|
1254
|
+
* Detect and execute the project's defined deployment pipeline:
|
|
1255
|
+
* 1. Commits any modified tracked files and pushes to `origin/<branch>`.
|
|
1256
|
+
* 2. Runs `deploy/deploy.sh` (or `scripts/deploy.sh`) if present in the repo (e.g. Meerlink cPanel SSH deploy).
|
|
1257
|
+
* 3. Or triggers GitHub Actions CI/CD via `git push`.
|
|
1258
|
+
*/
|
|
1259
|
+
function runProjectDeploymentPipeline(projectPath, rawTitle, shortId, forceEmptyCommit = false) {
|
|
1260
|
+
const cleanTitle = (rawTitle || 'update').replace(/["`\r\n]+/g, ' ').trim();
|
|
1261
|
+
let branch = 'main';
|
|
1262
|
+
let commitHash = '';
|
|
1263
|
+
const actions = [];
|
|
1214
1264
|
try {
|
|
1215
|
-
|
|
1216
|
-
card_id: card.id,
|
|
1217
|
-
card_updated_at: cardUpdatedAt,
|
|
1218
|
-
title: card.title,
|
|
1219
|
-
started_at: new Date().toISOString(),
|
|
1220
|
-
}), 'utf8');
|
|
1221
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
1265
|
+
branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
1222
1266
|
cwd: projectPath,
|
|
1223
1267
|
encoding: 'utf8',
|
|
1224
1268
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1225
|
-
}).trim();
|
|
1226
|
-
if (!branch || branch === 'HEAD')
|
|
1227
|
-
return false;
|
|
1269
|
+
}).trim() || 'main';
|
|
1228
1270
|
const statusOut = execSync('git status --porcelain -uno', {
|
|
1229
1271
|
cwd: projectPath,
|
|
1230
1272
|
encoding: 'utf8',
|
|
1231
1273
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1232
1274
|
}).trim();
|
|
1233
|
-
const rawText = `${card.title || ''} ${card.description || ''}`;
|
|
1234
|
-
const isContainerRestartCard = /\b(restart|redeploy|rebuild)\b.*\b(container|docker|server|prod)/i.test(rawText);
|
|
1235
1275
|
if (statusOut.length > 0) {
|
|
1236
1276
|
execSync('git add -u', { cwd: projectPath, stdio: 'ignore' });
|
|
1237
1277
|
execSync(`git commit -m "feat: ${cleanTitle} [AM-${shortId}]"`, {
|
|
@@ -1241,26 +1281,16 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
1241
1281
|
execSync(`git push origin ${branch}`, {
|
|
1242
1282
|
cwd: projectPath,
|
|
1243
1283
|
stdio: 'ignore',
|
|
1284
|
+
timeout: 45000,
|
|
1244
1285
|
});
|
|
1245
|
-
|
|
1286
|
+
commitHash = execSync('git rev-parse --short HEAD', {
|
|
1246
1287
|
cwd: projectPath,
|
|
1247
1288
|
encoding: 'utf8',
|
|
1248
1289
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1249
1290
|
}).trim();
|
|
1250
|
-
|
|
1251
|
-
await client.completeTask(card.id, provider, msg);
|
|
1252
|
-
appendIdeActivityLog(projectPath, {
|
|
1253
|
-
cardId: card.id,
|
|
1254
|
-
cardTitle: card.title,
|
|
1255
|
-
agentName: card.agent_display_name || provider,
|
|
1256
|
-
model: card.agent_model || 'auto',
|
|
1257
|
-
status: 'done',
|
|
1258
|
-
summary: `Deployment confirmed (${commitHash} -> origin/${branch}). Moved card to Done.`,
|
|
1259
|
-
});
|
|
1260
|
-
sendDesktopNotification(`Agile Monster -> Production Confirmed`, `Deployed "${card.title}" (${commitHash}) and moved to Done!`);
|
|
1261
|
-
return true;
|
|
1291
|
+
actions.push(`pushed \`${commitHash}\` to \`origin/${branch}\``);
|
|
1262
1292
|
}
|
|
1263
|
-
else if (
|
|
1293
|
+
else if (forceEmptyCommit) {
|
|
1264
1294
|
execSync(`git commit --allow-empty -m "chore: trigger container redeploy [AM-${shortId}]"`, {
|
|
1265
1295
|
cwd: projectPath,
|
|
1266
1296
|
stdio: 'ignore',
|
|
@@ -1268,39 +1298,97 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
1268
1298
|
execSync(`git push origin ${branch}`, {
|
|
1269
1299
|
cwd: projectPath,
|
|
1270
1300
|
stdio: 'ignore',
|
|
1301
|
+
timeout: 45000,
|
|
1271
1302
|
});
|
|
1272
|
-
|
|
1303
|
+
commitHash = execSync('git rev-parse --short HEAD', {
|
|
1273
1304
|
cwd: projectPath,
|
|
1274
1305
|
encoding: 'utf8',
|
|
1275
1306
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1276
1307
|
}).trim();
|
|
1277
|
-
|
|
1278
|
-
await client.completeTask(card.id, provider, msg);
|
|
1279
|
-
appendIdeActivityLog(projectPath, {
|
|
1280
|
-
cardId: card.id,
|
|
1281
|
-
cardTitle: card.title,
|
|
1282
|
-
agentName: card.agent_display_name || provider,
|
|
1283
|
-
model: card.agent_model || 'auto',
|
|
1284
|
-
status: 'done',
|
|
1285
|
-
summary: `Triggered container restart (${commitHash}) and moved card to Done.`,
|
|
1286
|
-
});
|
|
1287
|
-
sendDesktopNotification(`Agile Monster -> Container Redeploy`, `Triggered container restart (${commitHash}) on origin/${branch}!`);
|
|
1288
|
-
return true;
|
|
1308
|
+
actions.push(`triggered redeploy commit \`${commitHash}\` on \`origin/${branch}\``);
|
|
1289
1309
|
}
|
|
1290
1310
|
else {
|
|
1291
|
-
|
|
1311
|
+
commitHash = execSync('git rev-parse --short HEAD', {
|
|
1292
1312
|
cwd: projectPath,
|
|
1293
1313
|
encoding: 'utf8',
|
|
1294
1314
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1295
1315
|
}).trim();
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1316
|
+
}
|
|
1317
|
+
// Check if the project has a dedicated deploy script (e.g. deploy/deploy.sh in Meerlink)
|
|
1318
|
+
const deployScript = path.join(projectPath, 'deploy', 'deploy.sh');
|
|
1319
|
+
if (fs.existsSync(deployScript)) {
|
|
1320
|
+
const scriptContent = fs.readFileSync(deployScript, 'utf8');
|
|
1321
|
+
const flag = scriptContent.includes('--skip-preflight') ? ' --skip-preflight' : '';
|
|
1322
|
+
execSync(`bash "${deployScript}"${flag}`, {
|
|
1323
|
+
cwd: projectPath,
|
|
1324
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1325
|
+
timeout: 180000,
|
|
1326
|
+
});
|
|
1327
|
+
actions.push('deployed live via `deploy/deploy.sh`');
|
|
1328
|
+
}
|
|
1329
|
+
else if (fs.existsSync(path.join(projectPath, '.github', 'workflows'))) {
|
|
1330
|
+
actions.push('triggered GitHub Actions CI/CD deployment');
|
|
1331
|
+
}
|
|
1332
|
+
const deployedNote = actions.length > 0
|
|
1333
|
+
? `🚀 **Deployed**: Automatically ${actions.join(' and ')}.`
|
|
1334
|
+
: `🚀 **Deployed**: Verified commit \`${commitHash}\` is live on \`origin/${branch}\`.`;
|
|
1335
|
+
return { ok: true, commitHash, branch, deployedNote };
|
|
1336
|
+
}
|
|
1337
|
+
catch (err) {
|
|
1338
|
+
return {
|
|
1339
|
+
ok: false,
|
|
1340
|
+
commitHash,
|
|
1341
|
+
branch,
|
|
1342
|
+
deployedNote: '',
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
1347
|
+
if (!projectPath || !fs.existsSync(projectPath))
|
|
1348
|
+
return false;
|
|
1349
|
+
const shortId = card.id.slice(0, 8);
|
|
1350
|
+
const deployLock = path.join(getAgileMonsterLocksDir(), `deployed-${shortId}.json`);
|
|
1351
|
+
const cardUpdatedAt = card.updated_at || '';
|
|
1352
|
+
if (fs.existsSync(deployLock)) {
|
|
1353
|
+
try {
|
|
1354
|
+
const existing = JSON.parse(fs.readFileSync(deployLock, 'utf8'));
|
|
1355
|
+
if (!cardUpdatedAt || existing?.card_updated_at === cardUpdatedAt) {
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
return false;
|
|
1299
1361
|
}
|
|
1300
1362
|
}
|
|
1363
|
+
const provider = card.agent_provider || 'cursor';
|
|
1364
|
+
const rawText = `${card.title || ''} ${card.description || ''}`;
|
|
1365
|
+
const isContainerRestartCard = /\b(restart|redeploy|rebuild)\b.*\b(container|docker|server|prod)/i.test(rawText);
|
|
1366
|
+
try {
|
|
1367
|
+
fs.writeFileSync(deployLock, JSON.stringify({
|
|
1368
|
+
card_id: card.id,
|
|
1369
|
+
card_updated_at: cardUpdatedAt,
|
|
1370
|
+
title: card.title,
|
|
1371
|
+
started_at: new Date().toISOString(),
|
|
1372
|
+
}), 'utf8');
|
|
1373
|
+
const res = runProjectDeploymentPipeline(projectPath, card.title || 'update', shortId, isContainerRestartCard);
|
|
1374
|
+
const msg = res.deployedNote
|
|
1375
|
+
? `${res.deployedNote} Confirmed and moved card to **Done**.`
|
|
1376
|
+
: `🚀 **Deployed**: Confirmed latest commit \`${res.commitHash}\` is live on \`origin/${res.branch}\` and moved card to **Done**.`;
|
|
1377
|
+
await client.completeTask(card.id, provider, msg);
|
|
1378
|
+
appendIdeActivityLog(projectPath, {
|
|
1379
|
+
cardId: card.id,
|
|
1380
|
+
cardTitle: card.title,
|
|
1381
|
+
agentName: card.agent_display_name || provider,
|
|
1382
|
+
model: card.agent_model || 'auto',
|
|
1383
|
+
status: 'done',
|
|
1384
|
+
summary: msg,
|
|
1385
|
+
});
|
|
1386
|
+
sendDesktopNotification(`Agile Monster -> Production Confirmed`, `Deployed "${card.title}" (${res.commitHash}) and moved to Done!`);
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1301
1389
|
catch (err) {
|
|
1302
1390
|
try {
|
|
1303
|
-
await client.addProgress(card.id, provider, `⚠️ **[Auto-Deploy Note]** Attempted to
|
|
1391
|
+
await client.addProgress(card.id, provider, `⚠️ **[Auto-Deploy Note]** Attempted to deploy approved changes in \`${projectPath}\`: ${err?.message || String(err)}`);
|
|
1304
1392
|
}
|
|
1305
1393
|
catch { }
|
|
1306
1394
|
return false;
|
|
@@ -1446,6 +1534,23 @@ export async function runBackgroundPulseDaemon(intervalMs = 20000) {
|
|
|
1446
1534
|
const client = new AgileMonsterClient();
|
|
1447
1535
|
console.log(`[Agile Monster Pulse Daemon v${MCP_SERVER_VERSION}] Listening for queued agent tasks across all bound repositories (interval: ${intervalMs / 1000}s)...`);
|
|
1448
1536
|
startBuiltInPullPulse(null, client, intervalMs);
|
|
1449
|
-
|
|
1450
|
-
|
|
1537
|
+
const selfScript = process.argv[1] || '';
|
|
1538
|
+
const initialMtime = selfScript && fs.existsSync(selfScript) ? fs.statSync(selfScript).mtimeMs : 0;
|
|
1539
|
+
// Auto-restart daemon if mcp-server binary/dist on disk is updated so it never runs stale code in memory
|
|
1540
|
+
setInterval(() => {
|
|
1541
|
+
try {
|
|
1542
|
+
if (selfScript && initialMtime > 0 && fs.existsSync(selfScript)) {
|
|
1543
|
+
const curMtime = fs.statSync(selfScript).mtimeMs;
|
|
1544
|
+
if (curMtime > initialMtime + 1000) {
|
|
1545
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
1546
|
+
detached: true,
|
|
1547
|
+
stdio: 'ignore',
|
|
1548
|
+
});
|
|
1549
|
+
child.unref();
|
|
1550
|
+
process.exit(0);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
catch { }
|
|
1555
|
+
}, 30000);
|
|
1451
1556
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agilemonster/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Official Model Context Protocol (MCP) server & Orchestrator for Agile Monster (Cursor, Claude Code, Antigravity, Codex, Hermes, Windsurf)",
|
|
6
6
|
"main": "dist/server.js",
|