@agilemonster/mcp-server 1.0.12 → 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.12";
2
+ export declare const MCP_SERVER_VERSION = "1.0.15";
3
3
  export declare class AgileMonsterClient {
4
4
  private apiKey;
5
5
  private baseUrl;
@@ -36,7 +36,20 @@ export declare class AgileMonsterClient {
36
36
  projectName?: string;
37
37
  projectPath?: string;
38
38
  discoveredProjects?: DiscoveredProject[];
39
+ discoveredModels?: Record<string, {
40
+ default_model?: string | null;
41
+ models: Record<string, string> | string[];
42
+ }>;
39
43
  }): Promise<PulseResponse>;
44
+ /**
45
+ * Discover live models installed/configured across the developer's local AI coding agents
46
+ * (Codex, Cursor, Claude Code, Hermes, Antigravity) so the Agile Monster app & website
47
+ * always display the real models available for each selected agent.
48
+ */
49
+ discoverLocalAgentModels(): Record<string, {
50
+ default_model?: string | null;
51
+ models: Record<string, string>;
52
+ }>;
40
53
  /**
41
54
  * Scan the developer's local workspace roots (sibling folders in parent dev dir, ~/dev, ~/Projects, etc.)
42
55
  * to discover real project folders so the web UI can auto-match and let the user pick from a dropdown.
@@ -51,7 +64,11 @@ export declare class AgileMonsterClient {
51
64
  addProgress(cardId: string, provider: string, update: string, tokensUsed?: number, costUsd?: number): Promise<Card>;
52
65
  recordSpend(cardId: string, provider: string, tokens: number, costUsd: number, note?: string): Promise<Card>;
53
66
  setBudget(cardId: string, maxBudgetUsd?: number | null, maxTokens?: number | null): Promise<Card>;
54
- submitReview(cardId: string, provider: string, summary: string, verificationSteps?: string, targetList?: string, assignToUserId?: string): Promise<Card>;
67
+ submitReview(cardId: string, provider: string, summary: string, verificationSteps?: string, targetList?: string, assignToUserId?: string, extra?: {
68
+ diffText?: string;
69
+ logText?: string;
70
+ filesModified?: string;
71
+ }): Promise<Card>;
55
72
  completeTask(cardId: string, provider: string, comment?: string): Promise<Card>;
56
73
  blockTask(cardId: string, provider: string, comment?: string): Promise<Card>;
57
74
  createFollowupTasks(cardId: string, provider: string, tasks: Array<{
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.12';
4
+ export const MCP_SERVER_VERSION = '1.0.15';
5
5
  export class AgileMonsterClient {
6
6
  apiKey = '';
7
7
  baseUrl;
@@ -196,9 +196,136 @@ export class AgileMonsterClient {
196
196
  project_path: options.projectPath,
197
197
  mcp_version: MCP_SERVER_VERSION,
198
198
  discovered_projects: options.discoveredProjects,
199
+ discovered_models: options.discoveredModels,
199
200
  }),
200
201
  });
201
202
  }
203
+ /**
204
+ * Discover live models installed/configured across the developer's local AI coding agents
205
+ * (Codex, Cursor, Claude Code, Hermes, Antigravity) so the Agile Monster app & website
206
+ * always display the real models available for each selected agent.
207
+ */
208
+ discoverLocalAgentModels() {
209
+ const homedir = process.env.HOME || process.env.USERPROFILE || '';
210
+ const result = {};
211
+ if (!homedir)
212
+ return result;
213
+ // 1. Codex (~/.codex/models_cache.json & ~/.codex/config.toml)
214
+ try {
215
+ const codexCachePath = path.join(homedir, '.codex', 'models_cache.json');
216
+ const codexConfigPath = path.join(homedir, '.codex', 'config.toml');
217
+ let defaultCodex = null;
218
+ if (fs.existsSync(codexConfigPath)) {
219
+ const toml = fs.readFileSync(codexConfigPath, 'utf8');
220
+ const m = toml.match(/^\s*model\s*=\s*["']([^"']+)["']/m);
221
+ if (m?.[1])
222
+ defaultCodex = m[1].trim();
223
+ }
224
+ if (fs.existsSync(codexCachePath)) {
225
+ const raw = JSON.parse(fs.readFileSync(codexCachePath, 'utf8'));
226
+ const list = Array.isArray(raw?.models) ? raw.models : [];
227
+ const models = {};
228
+ for (const item of list) {
229
+ const slug = String(item?.slug || item?.id || '').trim();
230
+ if (!slug)
231
+ continue;
232
+ const label = String(item?.display_name || slug).trim();
233
+ models[slug] = label;
234
+ }
235
+ if (Object.keys(models).length > 0) {
236
+ result.codex = { default_model: defaultCodex, models };
237
+ }
238
+ }
239
+ }
240
+ catch { }
241
+ // 2. Cursor (~/Library/Application Support/Cursor/User/globalStorage/state.vscdb)
242
+ try {
243
+ const cursorDb = path.join(homedir, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
244
+ if (fs.existsSync(cursorDb)) {
245
+ const out = execSync(`sqlite3 "${cursorDb}" "SELECT value FROM ItemTable WHERE key = 'src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser' LIMIT 1;"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 4000 }).trim();
246
+ if (out) {
247
+ const data = JSON.parse(out);
248
+ const aiSettings = data?.aiSettings || {};
249
+ const defaultCursor = aiSettings?.modelConfig?.composer?.modelName ||
250
+ aiSettings?.openAIModel ||
251
+ null;
252
+ const avail = Array.isArray(data?.availableDefaultModels2) ? data.availableDefaultModels2 : [];
253
+ const models = {};
254
+ for (const m of avail) {
255
+ const name = String(m?.name || '').trim();
256
+ if (!name)
257
+ continue;
258
+ const label = String(m?.clientDisplayName || m?.serverModelName || name).trim();
259
+ models[name] = label;
260
+ }
261
+ if (Object.keys(models).length > 0) {
262
+ result.cursor = { default_model: defaultCursor, models };
263
+ }
264
+ }
265
+ }
266
+ }
267
+ catch { }
268
+ // 3. Claude Code (~/.claude.json)
269
+ try {
270
+ const claudePath = path.join(homedir, '.claude.json');
271
+ if (fs.existsSync(claudePath)) {
272
+ const data = JSON.parse(fs.readFileSync(claudePath, 'utf8'));
273
+ const usageKeys = Object.keys(data?.CachedGrowthBookFeatures?.tengu_model_config || {});
274
+ const models = {
275
+ 'claude-opus-5-5': 'Claude Opus 5.5',
276
+ 'claude-sonnet-5-5': 'Claude Sonnet 5.5',
277
+ 'claude-haiku-5-5': 'Claude Haiku 5.5',
278
+ 'claude-opus-5': 'Claude Opus 5',
279
+ 'claude-sonnet-5': 'Claude Sonnet 5',
280
+ 'claude-fable-5-1': 'Claude Fable 5.1',
281
+ 'claude-opus-4-8': 'Claude Opus 4.8',
282
+ 'claude-opus-4-7': 'Claude Opus 4.7',
283
+ 'claude-opus-4-6': 'Claude Opus 4.6',
284
+ 'claude-sonnet-4-6': 'Claude Sonnet 4.6',
285
+ 'claude-haiku-4-5': 'Claude Haiku 4.5',
286
+ };
287
+ for (const k of usageKeys) {
288
+ if (k.startsWith('claude-') && !models[k]) {
289
+ models[k] = k;
290
+ }
291
+ }
292
+ result.claude_code = { default_model: 'claude-sonnet-5-5', models };
293
+ }
294
+ }
295
+ catch { }
296
+ // 4. Hermes (~/.hermes/config.yaml)
297
+ try {
298
+ const hermesPath = path.join(homedir, '.hermes', 'config.yaml');
299
+ if (fs.existsSync(hermesPath)) {
300
+ const yaml = fs.readFileSync(hermesPath, 'utf8');
301
+ const defMatch = yaml.match(/^\s*default\s*:\s*([^\r\n#]+)/m);
302
+ const defModel = defMatch?.[1] ? defMatch[1].trim().replace(/^["']|["']$/g, '') : 'qwen/qwen3.8-27b';
303
+ const models = {};
304
+ if (defModel)
305
+ models[defModel] = `${defModel} (Default)`;
306
+ models['qwen/qwen3.8-27b'] = 'Qwen 3.8 27B (LM Studio)';
307
+ models['qwen3.5:397b'] = 'Qwen 3.5 397B';
308
+ models['glm-4.7'] = 'GLM 4.7';
309
+ models['theta-1'] = 'Theta 1';
310
+ models['claude-opus-4-6'] = 'Claude Opus 4.6';
311
+ models['google/gemini-3-flash-preview'] = 'Gemini 3 Flash Preview';
312
+ result.hermes = { default_model: defModel, models };
313
+ }
314
+ }
315
+ catch { }
316
+ // 5. Antigravity
317
+ result.antigravity = {
318
+ default_model: 'gemini-3.1-pro',
319
+ models: {
320
+ 'gemini-3.1-pro': 'Gemini 3.1 Pro',
321
+ 'gemini-3.0-flash': 'Gemini 3.0 Flash',
322
+ 'claude-opus-5-5': 'Claude Opus 5.5',
323
+ 'claude-sonnet-5-5': 'Claude Sonnet 5.5',
324
+ 'gpt-6.1-sol': 'GPT-6.1 Sol',
325
+ },
326
+ };
327
+ return result;
328
+ }
202
329
  /**
203
330
  * Scan the developer's local workspace roots (sibling folders in parent dev dir, ~/dev, ~/Projects, etc.)
204
331
  * to discover real project folders so the web UI can auto-match and let the user pick from a dropdown.
@@ -344,7 +471,7 @@ export class AgileMonsterClient {
344
471
  }),
345
472
  });
346
473
  }
347
- async submitReview(cardId, provider, summary, verificationSteps, targetList, assignToUserId) {
474
+ async submitReview(cardId, provider, summary, verificationSteps, targetList, assignToUserId, extra) {
348
475
  return this.request(`/api/agent/cards/${cardId}/submit-review`, {
349
476
  method: 'POST',
350
477
  body: JSON.stringify({
@@ -355,6 +482,9 @@ export class AgileMonsterClient {
355
482
  target_list_id: targetList,
356
483
  target_list_name: targetList,
357
484
  assign_to_user_id: assignToUserId,
485
+ diff_text: extra?.diffText,
486
+ log_text: extra?.logText,
487
+ files_modified: extra?.filesModified,
358
488
  }),
359
489
  });
360
490
  }
package/dist/server.js CHANGED
@@ -892,20 +892,79 @@ function getAgileMonsterLocksDir() {
892
892
  catch { }
893
893
  return dir;
894
894
  }
895
+ /**
896
+ * Append a live, human-readable IDE activity entry to `<projectPath>/.agilemonster/ACTIVITY.md`
897
+ * (and exclude `.agilemonster/` in `.git/info/exclude` so it never pollutes `git status`).
898
+ * Developers can open `.agilemonster/ACTIVITY.md` in their IDE at any time to see full agent
899
+ * troubleshooting logs, files inspected, build verification output, and diffs.
900
+ */
901
+ function appendIdeActivityLog(projectPath, entry) {
902
+ try {
903
+ const amDir = path.join(projectPath, '.agilemonster');
904
+ if (!fs.existsSync(amDir)) {
905
+ fs.mkdirSync(amDir, { recursive: true });
906
+ }
907
+ const gitExclude = path.join(projectPath, '.git', 'info', 'exclude');
908
+ if (fs.existsSync(gitExclude)) {
909
+ const exContent = fs.readFileSync(gitExclude, 'utf8');
910
+ if (!exContent.includes('.agilemonster/')) {
911
+ fs.appendFileSync(gitExclude, '\n.agilemonster/\n.agilemonster.json\n', 'utf8');
912
+ }
913
+ }
914
+ const logPath = path.join(amDir, 'ACTIVITY.md');
915
+ const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
916
+ const header = !fs.existsSync(logPath)
917
+ ? `# 🤖 Agile Monster — Live IDE Agent Activity Log\n\nThis file is automatically updated by the Agile Monster agent daemon so you can inspect every task, model, file change, and build check directly in your IDE.\n\n---\n\n`
918
+ : '';
919
+ const lines = [
920
+ `## [${timestamp}] ${entry.cardTitle} (\`${entry.cardId.slice(0, 8)}\`)`,
921
+ `- **Agent**: ${entry.agentName} | **Model**: \`${entry.model || 'auto'}\` | **Status**: \`${entry.status}\``,
922
+ ];
923
+ if (entry.files && entry.files.length > 0) {
924
+ lines.push(`- **Files**: ${entry.files.map((f) => `\`${f}\``).join(', ')}`);
925
+ }
926
+ lines.push(`- **Summary**: ${entry.summary}`);
927
+ if (entry.diff && entry.diff.trim().length > 0) {
928
+ const clippedDiff = entry.diff.length > 4000 ? entry.diff.slice(0, 4000) + '\n... (truncated)' : entry.diff;
929
+ lines.push('', '```diff', clippedDiff.trim(), '```');
930
+ }
931
+ lines.push('', '---', '');
932
+ fs.appendFileSync(logPath, header + lines.join('\n'), 'utf8');
933
+ }
934
+ catch { }
935
+ }
895
936
  export async function executeQueuedCardAutonomously(client, card, projectPath) {
896
937
  if (!projectPath || !fs.existsSync(projectPath))
897
938
  return false;
898
939
  if (activeAutonomousCards.has(card.id))
899
940
  return false;
900
941
  const lockFile = path.join(getAgileMonsterLocksDir(), `task-${card.id.slice(0, 8)}.lock`);
942
+ const cardUpdatedAt = card.updated_at || '';
901
943
  try {
902
944
  if (fs.existsSync(lockFile)) {
903
945
  const stat = fs.statSync(lockFile);
904
- if (Date.now() - stat.mtimeMs < 10 * 60 * 1000) {
905
- return false; // Already running or recently processed
946
+ let existingLock = {};
947
+ try {
948
+ existingLock = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
949
+ }
950
+ catch { }
951
+ const sameRevision = cardUpdatedAt !== '' && existingLock?.card_updated_at === cardUpdatedAt;
952
+ if (existingLock?.status === 'running' && Date.now() - stat.mtimeMs < 3 * 60 * 1000 && sameRevision) {
953
+ return false; // Currently running on this exact revision
954
+ }
955
+ if (existingLock?.status === 'completed' && sameRevision && Date.now() - stat.mtimeMs < 10 * 60 * 1000) {
956
+ return false; // Already completed this exact revision
957
+ }
958
+ if (!cardUpdatedAt && Date.now() - stat.mtimeMs < 3 * 60 * 1000) {
959
+ return false;
906
960
  }
907
961
  }
908
- fs.writeFileSync(lockFile, JSON.stringify({ card_id: card.id, started_at: new Date().toISOString() }), 'utf8');
962
+ fs.writeFileSync(lockFile, JSON.stringify({
963
+ card_id: card.id,
964
+ card_updated_at: cardUpdatedAt,
965
+ status: 'running',
966
+ started_at: new Date().toISOString(),
967
+ }), 'utf8');
909
968
  }
910
969
  catch {
911
970
  return false;
@@ -929,15 +988,29 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
929
988
  await client.addProgress(card.id, provider, `Card is now **In Progress**, but no matching source files were found in \`${projectPath}\`.`);
930
989
  return false;
931
990
  }
932
- // 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
933
992
  const selectionRes = await client.autonomousStep({
934
993
  card_id: card.id,
935
994
  action: 'select_files',
936
995
  file_index: fileIndex,
937
996
  });
938
- const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0, 4);
997
+ const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0, 6);
939
998
  if (selectedFiles.length === 0) {
940
- 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
+ }
941
1014
  }
942
1015
  const filesPayload = {};
943
1016
  for (const rel of selectedFiles) {
@@ -956,96 +1029,217 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
956
1029
  .map((f) => `\`${f}\``)
957
1030
  .join(', ')}. Generating code changes...`);
958
1031
  // 4. Generate surgical code edits
959
- const editsRes = await client.autonomousStep({
1032
+ let editsRes = await client.autonomousStep({
960
1033
  card_id: card.id,
961
1034
  action: 'generate_edits',
962
1035
  files: filesPayload,
963
1036
  });
964
- const edits = editsRes.edits || [];
965
- const modifiedFiles = [];
966
- const originalContents = new Map();
967
- for (const edit of edits) {
968
- const relPath = edit.file || edit.path;
969
- if (!relPath || !edit.search || typeof edit.replace !== 'string')
970
- continue;
971
- const fullPath = path.resolve(projectPath, relPath);
972
- if (!fullPath.startsWith(path.resolve(projectPath)))
973
- continue;
974
- if (!fs.existsSync(fullPath))
975
- continue;
976
- const currentContent = fs.readFileSync(fullPath, 'utf8');
977
- if (!originalContents.has(fullPath)) {
978
- originalContents.set(fullPath, currentContent);
979
- }
980
- if (currentContent.includes(edit.search)) {
981
- const updatedContent = currentContent.replace(edit.search, edit.replace);
982
- if (updatedContent !== currentContent) {
983
- // Guard against unbalanced braces/parens/brackets introduced by partial block replacement
984
- const countChar = (s, ch) => s.split(ch).length - 1;
985
- const searchBraceDelta = countChar(edit.search, '{') - countChar(edit.search, '}');
986
- const replaceBraceDelta = countChar(edit.replace, '{') - countChar(edit.replace, '}');
987
- const searchParenDelta = countChar(edit.search, '(') - countChar(edit.search, ')');
988
- const replaceParenDelta = countChar(edit.replace, '(') - countChar(edit.replace, ')');
989
- if (searchBraceDelta !== replaceBraceDelta || searchParenDelta !== replaceParenDelta) {
990
- continue;
991
- }
992
- fs.writeFileSync(fullPath, updatedContent, 'utf8');
993
- // Pre-submit static analysis gate for Dart/Flutter files
994
- if (relPath.endsWith('.dart')) {
995
- try {
996
- execSync(`dart analyze "${fullPath}"`, {
997
- cwd: projectPath,
998
- stdio: ['ignore', 'pipe', 'pipe'],
999
- timeout: 25000,
1000
- });
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;
1001
1062
  }
1002
- catch (analyzeErr) {
1003
- const out = String(analyzeErr?.stdout || '') + String(analyzeErr?.stderr || '');
1004
- if (/\berror\s+[•-]/i.test(out)) {
1005
- // 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 {
1006
1089
  fs.writeFileSync(fullPath, currentContent, 'utf8');
1007
1090
  continue;
1008
1091
  }
1009
1092
  }
1093
+ if (!modFiles.includes(relPath)) {
1094
+ modFiles.push(relPath);
1095
+ }
1010
1096
  }
1011
- if (!modifiedFiles.includes(relPath)) {
1012
- 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
+ });
1013
1129
  }
1014
1130
  }
1131
+ catch (buildErr) {
1132
+ const errOut = (String(buildErr?.stdout || '') + '\n' + String(buildErr?.stderr || '')).trim();
1133
+ return { ok: false, errorOutput: errOut.slice(-1500) };
1134
+ }
1135
+ }
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');
1144
+ }
1145
+ catch { }
1146
+ }
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) {
1161
+ for (const [fPath, orig] of originalContents.entries()) {
1162
+ try {
1163
+ fs.writeFileSync(fPath, orig, 'utf8');
1164
+ }
1165
+ catch { }
1166
+ }
1167
+ modifiedFiles.length = 0;
1015
1168
  }
1016
1169
  }
1017
1170
  if (modifiedFiles.length > 0) {
1018
- let diffSnippet = '';
1171
+ let rawDiff = '';
1019
1172
  try {
1020
1173
  const quotedFiles = modifiedFiles.map((f) => `"${f.replace(/"/g, '\\"')}"`).join(' ');
1021
- const rawDiff = execSync(`git diff -- ${quotedFiles}`, {
1174
+ rawDiff = execSync(`git diff -- ${quotedFiles}`, {
1022
1175
  cwd: projectPath,
1023
1176
  encoding: 'utf8',
1024
1177
  stdio: ['ignore', 'pipe', 'ignore'],
1025
1178
  }).trim();
1026
- if (rawDiff) {
1027
- const clipped = rawDiff.length > 2500 ? rawDiff.slice(0, 2500) + '\n... (diff truncated)' : rawDiff;
1028
- diffSnippet = `\n\n### Code Changes (\`git diff\`)\n\`\`\`diff\n${clipped}\n\`\`\``;
1029
- }
1030
1179
  }
1031
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));
1032
1184
  const baseSummary = editsRes.summary ||
1033
- `Implemented "${card.title}" autonomously across ${modifiedFiles.map((f) => `\`${f}\``).join(', ')}.`;
1034
- const summary = `${baseSummary}${diffSnippet}`;
1185
+ `Updated ${modifiedFiles.join(', ')} to complete "${card.title}".`;
1186
+ const cleanPlainSummary = baseSummary.replace(/```[\s\S]*?```/g, '').trim() +
1187
+ (deployInfo.deployedNote ? `\n\n${deployInfo.deployedNote}` : '');
1035
1188
  const verificationSteps = editsRes.verification_steps ||
1036
- `1. Review the code diff above in ${modifiedFiles.join(', ')}\n2. Click **Approve & Move to Production** (or drag to Production) to automatically commit, push, and deploy!`;
1037
- await client.submitReview(card.id, provider, summary, verificationSteps);
1189
+ `1. Check the live update for "${card.title}".\n2. Click **Approve & Deploy to Done** when verified.`;
1190
+ const logLines = [
1191
+ `Claimed card "${card.title}" (${card.id}) in ${projectPath}`,
1192
+ `Agent: ${workerLabel} | Model: ${card.agent_model || 'auto'}`,
1193
+ `Selected files: ${Object.keys(filesPayload).join(', ')}`,
1194
+ `Modified & verified files: ${modifiedFiles.join(', ')}`,
1195
+ ...(deployInfo.deployedNote ? [`Deployment: ${deployInfo.deployedNote}`] : []),
1196
+ `Summary: ${cleanPlainSummary}`,
1197
+ ];
1198
+ appendIdeActivityLog(projectPath, {
1199
+ cardId: card.id,
1200
+ cardTitle: card.title,
1201
+ agentName: workerLabel,
1202
+ model: card.agent_model || 'auto',
1203
+ status: 'review',
1204
+ summary: cleanPlainSummary,
1205
+ files: modifiedFiles,
1206
+ diff: rawDiff,
1207
+ });
1208
+ await client.submitReview(card.id, provider, cleanPlainSummary, verificationSteps, undefined, undefined, {
1209
+ diffText: rawDiff,
1210
+ logText: logLines.join('\n'),
1211
+ filesModified: modifiedFiles.join(', '),
1212
+ });
1213
+ try {
1214
+ fs.writeFileSync(lockFile, JSON.stringify({
1215
+ card_id: card.id,
1216
+ card_updated_at: cardUpdatedAt,
1217
+ status: 'completed',
1218
+ completed_at: new Date().toISOString(),
1219
+ }), 'utf8');
1220
+ }
1221
+ catch { }
1038
1222
  sendDesktopNotification(`Agile Monster -> Ready for Review`, `Completed "${card.title}" (${modifiedFiles.length} file(s) updated) and moved to Testing / Review!`);
1039
1223
  return true;
1040
1224
  }
1041
1225
  else {
1042
- await client.addProgress(card.id, provider, `Task claimed and moved to **In Progress**. Inspected ${Object.keys(filesPayload)
1226
+ try {
1227
+ if (fs.existsSync(lockFile))
1228
+ fs.unlinkSync(lockFile);
1229
+ }
1230
+ catch { }
1231
+ await client.askQuestion(card.id, provider, `Inspected ${Object.keys(filesPayload)
1043
1232
  .map((f) => `\`${f}\``)
1044
- .join(', ')}. Ready for final IDE review.`);
1233
+ .join(', ')}, but needed more specific details to apply a verified code change automatically. Reply in the comments with additional details (or open your IDE agent) to continue!`);
1045
1234
  return false;
1046
1235
  }
1047
1236
  }
1048
1237
  catch (err) {
1238
+ try {
1239
+ if (fs.existsSync(lockFile))
1240
+ fs.unlinkSync(lockFile);
1241
+ }
1242
+ catch { }
1049
1243
  try {
1050
1244
  await client.addProgress(card.id, provider, `Autonomous runner note: ${err?.message || String(err)}`);
1051
1245
  }
@@ -1056,32 +1250,28 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
1056
1250
  activeAutonomousCards.delete(card.id);
1057
1251
  }
1058
1252
  }
1059
- export async function deployApprovedCardIfNeeded(client, card, projectPath) {
1060
- if (!projectPath || !fs.existsSync(projectPath))
1061
- return false;
1062
- const shortId = card.id.slice(0, 8);
1063
- const deployLock = path.join(getAgileMonsterLocksDir(), `deployed-${shortId}.json`);
1064
- if (fs.existsSync(deployLock)) {
1065
- return false;
1066
- }
1067
- const provider = card.agent_provider || 'cursor';
1068
- 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 = [];
1069
1264
  try {
1070
- fs.writeFileSync(deployLock, JSON.stringify({ card_id: card.id, title: card.title, started_at: new Date().toISOString() }), 'utf8');
1071
- const branch = execSync('git rev-parse --abbrev-ref HEAD', {
1265
+ branch = execSync('git rev-parse --abbrev-ref HEAD', {
1072
1266
  cwd: projectPath,
1073
1267
  encoding: 'utf8',
1074
1268
  stdio: ['ignore', 'pipe', 'ignore'],
1075
- }).trim();
1076
- if (!branch || branch === 'HEAD')
1077
- return false;
1269
+ }).trim() || 'main';
1078
1270
  const statusOut = execSync('git status --porcelain -uno', {
1079
1271
  cwd: projectPath,
1080
1272
  encoding: 'utf8',
1081
1273
  stdio: ['ignore', 'pipe', 'ignore'],
1082
1274
  }).trim();
1083
- const rawText = `${card.title || ''} ${card.description || ''}`;
1084
- const isContainerRestartCard = /\b(restart|redeploy|rebuild)\b.*\b(container|docker|server|prod)/i.test(rawText);
1085
1275
  if (statusOut.length > 0) {
1086
1276
  execSync('git add -u', { cwd: projectPath, stdio: 'ignore' });
1087
1277
  execSync(`git commit -m "feat: ${cleanTitle} [AM-${shortId}]"`, {
@@ -1091,17 +1281,16 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
1091
1281
  execSync(`git push origin ${branch}`, {
1092
1282
  cwd: projectPath,
1093
1283
  stdio: 'ignore',
1284
+ timeout: 45000,
1094
1285
  });
1095
- const commitHash = execSync('git rev-parse --short HEAD', {
1286
+ commitHash = execSync('git rev-parse --short HEAD', {
1096
1287
  cwd: projectPath,
1097
1288
  encoding: 'utf8',
1098
1289
  stdio: ['ignore', 'pipe', 'ignore'],
1099
1290
  }).trim();
1100
- await client.addProgress(card.id, provider, `🚀 **[Auto-Deployed to Production]** Card approved into **${card.list_name || 'Production'}** — automatically committed and pushed \`${commitHash}\` to \`origin/${branch}\` to trigger the production CI/CD & container deployment pipeline!`);
1101
- sendDesktopNotification(`Agile Monster -> Production Deploy`, `Pushed "${card.title}" (${commitHash}) to origin/${branch}!`);
1102
- return true;
1291
+ actions.push(`pushed \`${commitHash}\` to \`origin/${branch}\``);
1103
1292
  }
1104
- else if (isContainerRestartCard) {
1293
+ else if (forceEmptyCommit) {
1105
1294
  execSync(`git commit --allow-empty -m "chore: trigger container redeploy [AM-${shortId}]"`, {
1106
1295
  cwd: projectPath,
1107
1296
  stdio: 'ignore',
@@ -1109,29 +1298,97 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
1109
1298
  execSync(`git push origin ${branch}`, {
1110
1299
  cwd: projectPath,
1111
1300
  stdio: 'ignore',
1301
+ timeout: 45000,
1112
1302
  });
1113
- const commitHash = execSync('git rev-parse --short HEAD', {
1303
+ commitHash = execSync('git rev-parse --short HEAD', {
1114
1304
  cwd: projectPath,
1115
1305
  encoding: 'utf8',
1116
1306
  stdio: ['ignore', 'pipe', 'ignore'],
1117
1307
  }).trim();
1118
- await client.addProgress(card.id, provider, `🚀 **[Auto-Deployed to Production]** Triggered production container rebuild & restart via \`${commitHash}\` on \`origin/${branch}\`.`);
1119
- sendDesktopNotification(`Agile Monster -> Container Redeploy`, `Triggered container restart (${commitHash}) on origin/${branch}!`);
1120
- return true;
1308
+ actions.push(`triggered redeploy commit \`${commitHash}\` on \`origin/${branch}\``);
1121
1309
  }
1122
1310
  else {
1123
- const headHash = execSync('git rev-parse --short HEAD', {
1311
+ commitHash = execSync('git rev-parse --short HEAD', {
1124
1312
  cwd: projectPath,
1125
1313
  encoding: 'utf8',
1126
1314
  stdio: ['ignore', 'pipe', 'ignore'],
1127
1315
  }).trim();
1128
- await client.addProgress(card.id, provider, `🚀 **[Auto-Deployed to Production]** Verified latest commit \`${headHash}\` is pushed to \`origin/${branch}\` and live in the production CI/CD pipeline.`);
1129
- return true;
1130
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;
1361
+ }
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;
1131
1388
  }
1132
1389
  catch (err) {
1133
1390
  try {
1134
- 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)}`);
1135
1392
  }
1136
1393
  catch { }
1137
1394
  return false;
@@ -1204,7 +1461,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
1204
1461
  if (!config?.board_id || !apiKey) {
1205
1462
  return;
1206
1463
  }
1207
- const includeDiscovery = pulseCount === 0 || pulseCount % 15 === 0;
1464
+ const includeDiscovery = pulseCount === 0 || pulseCount % 3 === 0;
1208
1465
  pulseCount++;
1209
1466
  const res = await client.pulse({
1210
1467
  boardId: config.board_id,
@@ -1212,6 +1469,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
1212
1469
  projectName: config.project_name || path.basename(process.cwd()),
1213
1470
  projectPath: config.project_path || process.cwd(),
1214
1471
  discoveredProjects: includeDiscovery ? client.discoverLocalProjects() : undefined,
1472
+ discoveredModels: includeDiscovery ? client.discoverLocalAgentModels() : undefined,
1215
1473
  });
1216
1474
  if (res.saved_bindings && res.saved_bindings.length > 0) {
1217
1475
  autoProvisionSavedBindings(apiKey, res.saved_bindings);
@@ -1276,6 +1534,23 @@ export async function runBackgroundPulseDaemon(intervalMs = 20000) {
1276
1534
  const client = new AgileMonsterClient();
1277
1535
  console.log(`[Agile Monster Pulse Daemon v${MCP_SERVER_VERSION}] Listening for queued agent tasks across all bound repositories (interval: ${intervalMs / 1000}s)...`);
1278
1536
  startBuiltInPullPulse(null, client, intervalMs);
1279
- // Keep process alive
1280
- 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);
1281
1556
  }
package/dist/types.d.ts CHANGED
@@ -30,6 +30,8 @@ export interface Card {
30
30
  board_name?: string;
31
31
  list_name?: string;
32
32
  created_by?: string;
33
+ created_at?: string;
34
+ updated_at?: string;
33
35
  bound_project_path?: string | null;
34
36
  bound_project_name?: string | null;
35
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agilemonster/mcp-server",
3
- "version": "1.0.12",
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",