@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 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.14";
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
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execSync } from 'node:child_process';
4
- export const MCP_SERVER_VERSION = '1.0.14';
4
+ export const MCP_SERVER_VERSION = '1.0.15';
5
5
  export class AgileMonsterClient {
6
6
  apiKey = '';
7
7
  baseUrl;
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-4 most relevant files
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, 4);
997
+ const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0, 6);
998
998
  if (selectedFiles.length === 0) {
999
- selectedFiles.push(...fileIndex.slice(0, 3));
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
- const editsRes = await client.autonomousStep({
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 edits = editsRes.edits || [];
1024
- const modifiedFiles = [];
1025
- const originalContents = new Map();
1026
- for (const edit of edits) {
1027
- const relPath = edit.file || edit.path;
1028
- if (!relPath || !edit.search || typeof edit.replace !== 'string')
1029
- continue;
1030
- const fullPath = path.resolve(projectPath, relPath);
1031
- if (!fullPath.startsWith(path.resolve(projectPath)))
1032
- continue;
1033
- if (!fs.existsSync(fullPath))
1034
- continue;
1035
- const currentContent = fs.readFileSync(fullPath, 'utf8');
1036
- if (!originalContents.has(fullPath)) {
1037
- originalContents.set(fullPath, currentContent);
1038
- }
1039
- if (currentContent.includes(edit.search)) {
1040
- const updatedContent = currentContent.replace(edit.search, edit.replace);
1041
- if (updatedContent !== currentContent) {
1042
- // Guard against unbalanced braces/parens/brackets introduced by partial block replacement
1043
- const countChar = (s, ch) => s.split(ch).length - 1;
1044
- const searchBraceDelta = countChar(edit.search, '{') - countChar(edit.search, '}');
1045
- const replaceBraceDelta = countChar(edit.replace, '{') - countChar(edit.replace, '}');
1046
- const searchParenDelta = countChar(edit.search, '(') - countChar(edit.search, ')');
1047
- const replaceParenDelta = countChar(edit.replace, '(') - countChar(edit.replace, ')');
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
- catch (analyzeErr) {
1062
- const out = String(analyzeErr?.stdout || '') + String(analyzeErr?.stderr || '');
1063
- if (/\berror\s+[•-]/i.test(out)) {
1064
- // Revert broken Dart edit so it never breaks the build
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
- else if (relPath.endsWith('.php')) {
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
- if (!modifiedFiles.includes(relPath)) {
1084
- modifiedFiles.push(relPath);
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
- // Pre-submit frontend build verification gate for JS/TS/React/Vite/Next.js projects
1090
- const hasFrontendEdits = modifiedFiles.some((f) => /\.(tsx?|jsx?|vue)$/i.test(f));
1091
- const pkgJsonPath = path.join(projectPath, 'package.json');
1092
- if (hasFrontendEdits && fs.existsSync(pkgJsonPath)) {
1093
- try {
1094
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
1095
- if (pkg?.scripts?.build && fs.existsSync(path.join(projectPath, 'node_modules'))) {
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
- catch (buildErr) {
1104
- // Revert broken frontend edits so we never leave the project with a broken build
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
- // Keep card comment concise plain English (no raw code diffs on the card!)
1128
- const cleanPlainSummary = baseSummary
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 updated behavior for "${card.title}".\n2. Click **Approve & Move to Done** to deploy and confirm.`;
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
- export async function deployApprovedCardIfNeeded(client, card, projectPath) {
1196
- if (!projectPath || !fs.existsSync(projectPath))
1197
- return false;
1198
- const shortId = card.id.slice(0, 8);
1199
- const deployLock = path.join(getAgileMonsterLocksDir(), `deployed-${shortId}.json`);
1200
- const cardUpdatedAt = card.updated_at || '';
1201
- if (fs.existsSync(deployLock)) {
1202
- try {
1203
- const existing = JSON.parse(fs.readFileSync(deployLock, 'utf8'));
1204
- if (!cardUpdatedAt || existing?.card_updated_at === cardUpdatedAt) {
1205
- return false;
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
- fs.writeFileSync(deployLock, JSON.stringify({
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
- const commitHash = execSync('git rev-parse --short HEAD', {
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
- const msg = `🚀 **[Auto-Deployed to Production]** Deployment confirmed! Committed and pushed \`${commitHash}\` to \`origin/${branch}\`, and moved card to **Done**.`;
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 (isContainerRestartCard) {
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
- const commitHash = execSync('git rev-parse --short HEAD', {
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
- const msg = `🚀 **[Auto-Deployed to Production]** Triggered production container rebuild & restart (\`${commitHash}\` on \`origin/${branch}\`) and confirmed move to **Done**.`;
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
- const headHash = execSync('git rev-parse --short HEAD', {
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
- const msg = `🚀 **[Auto-Deployed to Production]** Confirmed latest commit \`${headHash}\` is pushed to \`origin/${branch}\` and live in production. Moved card to **Done**.`;
1297
- await client.completeTask(card.id, provider, msg);
1298
- return true;
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 auto-push approved changes in \`${projectPath}\`: ${err?.message || String(err)}`);
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
- // Keep process alive
1450
- setInterval(() => { }, 60000);
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.14",
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",