@agilemonster/mcp-server 1.0.11 → 1.0.14

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.11";
2
+ export declare const MCP_SERVER_VERSION = "1.0.14";
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.11';
4
+ export const MCP_SERVER_VERSION = '1.0.14';
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
@@ -863,19 +863,24 @@ function collectProjectSourceFiles(projectPath, keywords) {
863
863
  }
864
864
  catch { }
865
865
  }
866
- // Bonus score for UI components / screens when task mentions UX or UI
866
+ // Bonus score for UI components / screens AND backend controllers / models
867
867
  if (ext === '.tsx' || ext === '.jsx' || ext === '.dart' || ext === '.vue') {
868
- if (lowerRel.includes('component') || lowerRel.includes('screen') || lowerRel.includes('widget') || lowerRel.includes('modal')) {
868
+ if (lowerRel.includes('component') || lowerRel.includes('screen') || lowerRel.includes('widget') || lowerRel.includes('modal') || lowerRel.includes('model')) {
869
869
  score += 8;
870
870
  }
871
871
  }
872
+ if (ext === '.php' || ext === '.py' || ext === '.ts') {
873
+ if (lowerRel.includes('controller') || lowerRel.includes('service') || lowerRel.includes('route')) {
874
+ score += 10;
875
+ }
876
+ }
872
877
  results.push({ rel, score });
873
878
  }
874
879
  }
875
880
  };
876
881
  walk(projectPath, 0);
877
882
  results.sort((a, b) => b.score - a.score || a.rel.localeCompare(b.rel));
878
- return results.slice(0, 220).map((r) => r.rel);
883
+ return results.slice(0, 250).map((r) => r.rel);
879
884
  }
880
885
  function getAgileMonsterLocksDir() {
881
886
  const dir = path.join(os.homedir(), '.agilemonster', 'locks');
@@ -887,20 +892,79 @@ function getAgileMonsterLocksDir() {
887
892
  catch { }
888
893
  return dir;
889
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
+ }
890
936
  export async function executeQueuedCardAutonomously(client, card, projectPath) {
891
937
  if (!projectPath || !fs.existsSync(projectPath))
892
938
  return false;
893
939
  if (activeAutonomousCards.has(card.id))
894
940
  return false;
895
941
  const lockFile = path.join(getAgileMonsterLocksDir(), `task-${card.id.slice(0, 8)}.lock`);
942
+ const cardUpdatedAt = card.updated_at || '';
896
943
  try {
897
944
  if (fs.existsSync(lockFile)) {
898
945
  const stat = fs.statSync(lockFile);
899
- if (Date.now() - stat.mtimeMs < 10 * 60 * 1000) {
900
- 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;
901
960
  }
902
961
  }
903
- 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');
904
968
  }
905
969
  catch {
906
970
  return false;
@@ -975,52 +1039,149 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
975
1039
  if (currentContent.includes(edit.search)) {
976
1040
  const updatedContent = currentContent.replace(edit.search, edit.replace);
977
1041
  if (updatedContent !== currentContent) {
978
- // Guard against unbalanced braces introduced by partial block replacement
1042
+ // Guard against unbalanced braces/parens/brackets introduced by partial block replacement
979
1043
  const countChar = (s, ch) => s.split(ch).length - 1;
980
1044
  const searchBraceDelta = countChar(edit.search, '{') - countChar(edit.search, '}');
981
1045
  const replaceBraceDelta = countChar(edit.replace, '{') - countChar(edit.replace, '}');
982
- if (searchBraceDelta !== replaceBraceDelta) {
1046
+ const searchParenDelta = countChar(edit.search, '(') - countChar(edit.search, ')');
1047
+ const replaceParenDelta = countChar(edit.replace, '(') - countChar(edit.replace, ')');
1048
+ if (searchBraceDelta !== replaceBraceDelta || searchParenDelta !== replaceParenDelta) {
983
1049
  continue;
984
1050
  }
985
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
+ });
1060
+ }
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
1065
+ fs.writeFileSync(fullPath, currentContent, 'utf8');
1066
+ continue;
1067
+ }
1068
+ }
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;
1081
+ }
1082
+ }
986
1083
  if (!modifiedFiles.includes(relPath)) {
987
1084
  modifiedFiles.push(relPath);
988
1085
  }
989
1086
  }
990
1087
  }
991
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
+ });
1101
+ }
1102
+ }
1103
+ catch (buildErr) {
1104
+ // Revert broken frontend edits so we never leave the project with a broken build
1105
+ for (const [fPath, orig] of originalContents.entries()) {
1106
+ try {
1107
+ fs.writeFileSync(fPath, orig, 'utf8');
1108
+ }
1109
+ catch { }
1110
+ }
1111
+ modifiedFiles.length = 0;
1112
+ }
1113
+ }
992
1114
  if (modifiedFiles.length > 0) {
993
- let diffSnippet = '';
1115
+ let rawDiff = '';
994
1116
  try {
995
1117
  const quotedFiles = modifiedFiles.map((f) => `"${f.replace(/"/g, '\\"')}"`).join(' ');
996
- const rawDiff = execSync(`git diff -- ${quotedFiles}`, {
1118
+ rawDiff = execSync(`git diff -- ${quotedFiles}`, {
997
1119
  cwd: projectPath,
998
1120
  encoding: 'utf8',
999
1121
  stdio: ['ignore', 'pipe', 'ignore'],
1000
1122
  }).trim();
1001
- if (rawDiff) {
1002
- const clipped = rawDiff.length > 2500 ? rawDiff.slice(0, 2500) + '\n... (diff truncated)' : rawDiff;
1003
- diffSnippet = `\n\n### Code Changes (\`git diff\`)\n\`\`\`diff\n${clipped}\n\`\`\``;
1004
- }
1005
1123
  }
1006
1124
  catch { }
1007
1125
  const baseSummary = editsRes.summary ||
1008
- `Implemented "${card.title}" autonomously across ${modifiedFiles.map((f) => `\`${f}\``).join(', ')}.`;
1009
- const summary = `${baseSummary}${diffSnippet}`;
1126
+ `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();
1010
1131
  const verificationSteps = editsRes.verification_steps ||
1011
- `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!`;
1012
- await client.submitReview(card.id, provider, summary, verificationSteps);
1132
+ `1. Check the updated behavior for "${card.title}".\n2. Click **Approve & Move to Done** to deploy and confirm.`;
1133
+ const logLines = [
1134
+ `Claimed card "${card.title}" (${card.id}) in ${projectPath}`,
1135
+ `Agent: ${workerLabel} | Model: ${card.agent_model || 'auto'}`,
1136
+ `Selected files: ${Object.keys(filesPayload).join(', ')}`,
1137
+ `Modified & verified files: ${modifiedFiles.join(', ')}`,
1138
+ `Summary: ${cleanPlainSummary}`,
1139
+ ];
1140
+ appendIdeActivityLog(projectPath, {
1141
+ cardId: card.id,
1142
+ cardTitle: card.title,
1143
+ agentName: workerLabel,
1144
+ model: card.agent_model || 'auto',
1145
+ status: 'review',
1146
+ summary: cleanPlainSummary,
1147
+ files: modifiedFiles,
1148
+ diff: rawDiff,
1149
+ });
1150
+ await client.submitReview(card.id, provider, cleanPlainSummary, verificationSteps, undefined, undefined, {
1151
+ diffText: rawDiff,
1152
+ logText: logLines.join('\n'),
1153
+ filesModified: modifiedFiles.join(', '),
1154
+ });
1155
+ try {
1156
+ fs.writeFileSync(lockFile, JSON.stringify({
1157
+ card_id: card.id,
1158
+ card_updated_at: cardUpdatedAt,
1159
+ status: 'completed',
1160
+ completed_at: new Date().toISOString(),
1161
+ }), 'utf8');
1162
+ }
1163
+ catch { }
1013
1164
  sendDesktopNotification(`Agile Monster -> Ready for Review`, `Completed "${card.title}" (${modifiedFiles.length} file(s) updated) and moved to Testing / Review!`);
1014
1165
  return true;
1015
1166
  }
1016
1167
  else {
1017
- await client.addProgress(card.id, provider, `Task claimed and moved to **In Progress**. Inspected ${Object.keys(filesPayload)
1168
+ try {
1169
+ if (fs.existsSync(lockFile))
1170
+ fs.unlinkSync(lockFile);
1171
+ }
1172
+ catch { }
1173
+ await client.askQuestion(card.id, provider, `Inspected ${Object.keys(filesPayload)
1018
1174
  .map((f) => `\`${f}\``)
1019
- .join(', ')}. Ready for final IDE review.`);
1175
+ .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!`);
1020
1176
  return false;
1021
1177
  }
1022
1178
  }
1023
1179
  catch (err) {
1180
+ try {
1181
+ if (fs.existsSync(lockFile))
1182
+ fs.unlinkSync(lockFile);
1183
+ }
1184
+ catch { }
1024
1185
  try {
1025
1186
  await client.addProgress(card.id, provider, `Autonomous runner note: ${err?.message || String(err)}`);
1026
1187
  }
@@ -1036,13 +1197,27 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
1036
1197
  return false;
1037
1198
  const shortId = card.id.slice(0, 8);
1038
1199
  const deployLock = path.join(getAgileMonsterLocksDir(), `deployed-${shortId}.json`);
1200
+ const cardUpdatedAt = card.updated_at || '';
1039
1201
  if (fs.existsSync(deployLock)) {
1040
- return false;
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
+ }
1041
1211
  }
1042
1212
  const provider = card.agent_provider || 'cursor';
1043
1213
  const cleanTitle = (card.title || 'update').replace(/["`\r\n]+/g, ' ').trim();
1044
1214
  try {
1045
- fs.writeFileSync(deployLock, JSON.stringify({ card_id: card.id, title: card.title, started_at: new Date().toISOString() }), 'utf8');
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');
1046
1221
  const branch = execSync('git rev-parse --abbrev-ref HEAD', {
1047
1222
  cwd: projectPath,
1048
1223
  encoding: 'utf8',
@@ -1072,8 +1247,17 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
1072
1247
  encoding: 'utf8',
1073
1248
  stdio: ['ignore', 'pipe', 'ignore'],
1074
1249
  }).trim();
1075
- 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!`);
1076
- sendDesktopNotification(`Agile Monster -> Production Deploy`, `Pushed "${card.title}" (${commitHash}) to origin/${branch}!`);
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!`);
1077
1261
  return true;
1078
1262
  }
1079
1263
  else if (isContainerRestartCard) {
@@ -1090,7 +1274,16 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
1090
1274
  encoding: 'utf8',
1091
1275
  stdio: ['ignore', 'pipe', 'ignore'],
1092
1276
  }).trim();
1093
- await client.addProgress(card.id, provider, `🚀 **[Auto-Deployed to Production]** Triggered production container rebuild & restart via \`${commitHash}\` on \`origin/${branch}\`.`);
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
+ });
1094
1287
  sendDesktopNotification(`Agile Monster -> Container Redeploy`, `Triggered container restart (${commitHash}) on origin/${branch}!`);
1095
1288
  return true;
1096
1289
  }
@@ -1100,7 +1293,8 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
1100
1293
  encoding: 'utf8',
1101
1294
  stdio: ['ignore', 'pipe', 'ignore'],
1102
1295
  }).trim();
1103
- 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.`);
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);
1104
1298
  return true;
1105
1299
  }
1106
1300
  }
@@ -1179,7 +1373,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
1179
1373
  if (!config?.board_id || !apiKey) {
1180
1374
  return;
1181
1375
  }
1182
- const includeDiscovery = pulseCount === 0 || pulseCount % 15 === 0;
1376
+ const includeDiscovery = pulseCount === 0 || pulseCount % 3 === 0;
1183
1377
  pulseCount++;
1184
1378
  const res = await client.pulse({
1185
1379
  boardId: config.board_id,
@@ -1187,6 +1381,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
1187
1381
  projectName: config.project_name || path.basename(process.cwd()),
1188
1382
  projectPath: config.project_path || process.cwd(),
1189
1383
  discoveredProjects: includeDiscovery ? client.discoverLocalProjects() : undefined,
1384
+ discoveredModels: includeDiscovery ? client.discoverLocalAgentModels() : undefined,
1190
1385
  });
1191
1386
  if (res.saved_bindings && res.saved_bindings.length > 0) {
1192
1387
  autoProvisionSavedBindings(apiKey, res.saved_bindings);
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.11",
3
+ "version": "1.0.14",
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",